From ad60eccf8babe76826d6e1f8e9e81eee62619a76 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sat, 3 Apr 2021 21:20:02 +0200 Subject: [PATCH 01/28] Add database migrations for exif values --- api/database/database.go | 7 ++ api/database/migration_exif.go | 169 +++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 api/database/migration_exif.go diff --git a/api/database/database.go b/api/database/database.go index b35e613..84f5b54 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -174,5 +174,12 @@ func MigrateDatabase(db *gorm.DB) error { db.Migrator().DropColumn(&models.Media{}, "date_imported") } + // v2.3.0 - Changed type of MediaEXIF.Exposure and MediaEXIF.Flash + // from string values to decimal and int respectively + err := migrate_exif_fields(db) + if err != nil { + return err + } + return nil } diff --git a/api/database/migration_exif.go b/api/database/migration_exif.go new file mode 100644 index 0000000..546157e --- /dev/null +++ b/api/database/migration_exif.go @@ -0,0 +1,169 @@ +package database + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/photoview/photoview/api/graphql/models" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +// Migrate MediaExif fields "exposure" and "flash" from strings to integers +func migrate_exif_fields(db *gorm.DB) error { + mediaExifColumns, err := db.Migrator().ColumnTypes(&models.MediaEXIF{}) + if err != nil { + return err + } + + err = db.Transaction(func(tx *gorm.DB) error { + for _, exifCol := range mediaExifColumns { + if exifCol.Name() == "exposure" { + switch exifCol.DatabaseTypeName() { + case "double", "numeric", "real": + // correct type, do nothing + default: + // do migration + if err := migrate_exif_fields_exposure(db); err != nil { + return err + } + } + } + + if exifCol.Name() == "flash" { + switch exifCol.DatabaseTypeName() { + case "double", "numeric", "real": + // correct type, do nothing + default: + // do migration + if err := migrate_exif_fields_flash(db); err != nil { + return err + } + } + } + } + + if err := db.AutoMigrate(&models.MediaEXIF{}); err != nil { + return errors.Wrap(err, "failed to auto migrate media_exif after exposure conversion") + } + + return nil + }) + + if err != nil { + return err + } + + return nil +} + +func migrate_exif_fields_exposure(db *gorm.DB) error { + log.Println("Migrating `media_exif.exposure` from string to double") + + err := db.Transaction(func(tx *gorm.DB) error { + + type exifModel struct { + ID int `gorm:"primarykey"` + Exposure *string + } + var results []exifModel + + return tx.Model(&exifModel{}).Table("media_exif").Where("exposure LIKE '%/%'").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error { + for _, result := range results { + frac := strings.Split(*result.Exposure, "/") + if len(frac) != 2 { + return errors.Errorf("failed to convert exposure value (%s) expected format x/y", frac) + } + + numerator, err := strconv.ParseFloat(frac[0], 64) + if err != nil { + return err + } + + denominator, err := strconv.ParseFloat(frac[1], 64) + if err != nil { + return err + } + + decimalValue := numerator / denominator + *result.Exposure = fmt.Sprintf("%f", decimalValue) + } + + tx.Save(&results) + + return nil + }).Error + }) + + if err != nil { + return errors.Wrap(err, "migrating `media_exif.exposure` failed") + } + + return nil +} + +func migrate_exif_fields_flash(db *gorm.DB) error { + log.Println("Migrating `media_exif.flash` from string to int") + + err := db.Transaction(func(tx *gorm.DB) error { + + type exifModel struct { + ID int `gorm:"primarykey"` + Flash *string + } + var results []exifModel + + var flashDescriptions = map[int]string{ + 0x0: "No Flash", + 0x1: "Fired", + 0x5: "Fired, Return not detected", + 0x7: "Fired, Return detected", + 0x8: "On, Did not fire", + 0x9: "On, Fired", + 0xD: "On, Return not detected", + 0xF: "On, Return detected", + 0x10: "Off, Did not fire", + 0x14: "Off, Did not fire, Return not detected", + 0x18: "Auto, Did not fire", + 0x19: "Auto, Fired", + 0x1D: "Auto, Fired, Return not detected", + 0x1F: "Auto, Fired, Return detected", + 0x20: "No flash function", + 0x30: "Off, No flash function", + 0x41: "Fired, Red-eye reduction", + 0x45: "Fired, Red-eye reduction, Return not detected", + 0x47: "Fired, Red-eye reduction, Return detected", + 0x49: "On, Red-eye reduction", + 0x4D: "On, Red-eye reduction, Return not detected", + 0x4F: "On, Red-eye reduction, Return detected", + 0x50: "Off, Red-eye reduction", + 0x58: "Auto, Did not fire, Red-eye reduction", + 0x59: "Auto, Fired, Red-eye reduction", + 0x5D: "Auto, Fired, Red-eye reduction, Return not detected", + 0x5F: "Auto, Fired, Red-eye reduction, Return detected", + } + + return tx.Model(&exifModel{}).Table("media_exif").Where("flash IS NOT NULL").FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error { + for _, result := range results { + for index, name := range flashDescriptions { + if *result.Flash == name { + *result.Flash = fmt.Sprintf("%d", index) + break + } + } + } + + tx.Save(&results) + + return nil + }).Error + }) + + if err != nil { + return errors.Wrap(err, "migrating `media_exif.flash` failed") + } + + return nil +} From ceb8c4103f41c2ca1884e506fea5c895b1c67c17 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sat, 3 Apr 2021 21:39:32 +0200 Subject: [PATCH 02/28] Fix exiftool detection --- api/scanner/exif/exif.go | 25 +++++++++++++++++++------ api/server.go | 3 +++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/api/scanner/exif/exif.go b/api/scanner/exif/exif.go index 2980d8a..082f26c 100644 --- a/api/scanner/exif/exif.go +++ b/api/scanner/exif/exif.go @@ -14,6 +14,22 @@ type exifParser interface { ParseExif(media *models.Media) (*models.MediaEXIF, error) } +var use_exiftool bool = false + +func InitializeEXIFParser() { + // Decide between internal or external Exif parser + et, err := exiftool.NewExiftool() + + if err != nil { + use_exiftool = false + log.Printf("Failed to get exiftool, using internal exif parser instead: %v\n", err) + } else { + et.Close() + log.Println("Found exiftool") + use_exiftool = true + } +} + // SaveEXIF scans the media file for exif metadata and saves it in the database if found func SaveEXIF(tx *gorm.DB, media *models.Media) (*models.MediaEXIF, error) { @@ -32,14 +48,11 @@ func SaveEXIF(tx *gorm.DB, media *models.Media) (*models.MediaEXIF, error) { } } - // Decide between internal or external Exif parser - et, err := exiftool.NewExiftool() - et.Close() var parser exifParser - if err != nil { - parser = &internalExifParser{} - } else { + if use_exiftool { parser = &externalExifParser{} + } else { + parser = &internalExifParser{} } exif, err := parser.ParseExif(media) diff --git a/api/server.go b/api/server.go index e6380aa..c423d33 100644 --- a/api/server.go +++ b/api/server.go @@ -16,6 +16,7 @@ import ( "github.com/photoview/photoview/api/graphql/dataloader" "github.com/photoview/photoview/api/routes" "github.com/photoview/photoview/api/scanner" + "github.com/photoview/photoview/api/scanner/exif" "github.com/photoview/photoview/api/scanner/face_detection" "github.com/photoview/photoview/api/server" "github.com/photoview/photoview/api/utils" @@ -55,6 +56,8 @@ func main() { scanner.InitializeExecutableWorkers() + exif.InitializeEXIFParser() + if err := face_detection.InitializeFaceDetector(db); err != nil { log.Panicf("Could not initialize face detector: %s\n", err) } From 48605e60e047adf9aa947f1d35e44a96848ded49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Strate=20Kl=C3=B8vedal?= Date: Sat, 3 Apr 2021 21:49:27 +0200 Subject: [PATCH 03/28] Add @robin-moser to sponsors section in readme --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1c6a545..9231797 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,18 @@ And the graphql playground at [localhost:4001](http://localhost:4001) ## Sponsors - -
- @ericerkz -
+ + + + +
+ +
+ @ericerkz +
+
+ +
+ @robin-moser +
+
From 8ec2a7789bc081cde5e967efd3271baddddb1154 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sat, 3 Apr 2021 22:52:53 +0200 Subject: [PATCH 04/28] Fix sidecar hash bug This closes #276 --- api/graphql/models/media.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go index 3f03cd9..8fdf610 100644 --- a/api/graphql/models/media.go +++ b/api/graphql/models/media.go @@ -39,14 +39,9 @@ func (Media) TableName() string { } func (m *Media) BeforeSave(tx *gorm.DB) error { - // Update hashes + // Update path hash m.PathHash = MD5Hash(m.Path) - if m.SideCarPath != nil { - encodedHash := MD5Hash(*m.SideCarPath) - m.SideCarHash = &encodedHash - } - return nil } From a2c13d6fa1df5e15070d135981ef33ec842b130f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Strate=20Kl=C3=B8vedal?= Date: Sat, 3 Apr 2021 23:13:29 +0200 Subject: [PATCH 05/28] Fix error in build action when pushing release --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12b6545..f77ad55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -122,7 +122,7 @@ jobs: if [[ $VERSION =~ ^(([0-9]{1,3})\.[0-9]{1,3})\.[0-9]{1,3}$ ]]; then VERSION_MINOR=${BASH_REMATCH[1]} VERSION_MAJOR=${BASH_REMATCH[2]} - TAGS+=("${VERSION_MAJOR}", "${VERSION_MINOR}") + TAGS+=("${VERSION_MAJOR}" "${VERSION_MINOR}") fi for TAG in ${TAGS[*]}; do From 8a1f2e957ee3165934726506205814c4f4101d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Strate=20Kl=C3=B8vedal?= Date: Sat, 3 Apr 2021 23:30:08 +0200 Subject: [PATCH 06/28] Add "latest" tag when building release --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f77ad55..997c392 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,6 +97,9 @@ jobs: name: Combine Docker Images runs-on: ubuntu-20.04 if: github.event_name != 'pull_request' && github.repository == 'photoview/photoview' + defaults: + run: + shell: bash needs: [build] @@ -122,7 +125,7 @@ jobs: if [[ $VERSION =~ ^(([0-9]{1,3})\.[0-9]{1,3})\.[0-9]{1,3}$ ]]; then VERSION_MINOR=${BASH_REMATCH[1]} VERSION_MAJOR=${BASH_REMATCH[2]} - TAGS+=("${VERSION_MAJOR}" "${VERSION_MINOR}") + TAGS+=("${VERSION_MAJOR}" "${VERSION_MINOR}" "latest") fi for TAG in ${TAGS[*]}; do From ac8632cd8204b0241a07552ec4b497c11dd2a9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Strate=20Kl=C3=B8vedal?= Date: Sun, 4 Apr 2021 10:27:09 +0200 Subject: [PATCH 07/28] Add event_name to docker tags in build action --- .github/workflows/build.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 997c392..4670fd8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: OUTPUT_PLATFORM=$(echo ${{ matrix.target_platform }} | sed 's/\//-/g') echo ::set-output name=output_platform::${OUTPUT_PLATFORM} - TAG="--tag ${DOCKER_IMAGE}:${OUTPUT_PLATFORM}-${GITHUB_SHA::8}" + TAG="--tag ${DOCKER_IMAGE}:${OUTPUT_PLATFORM}-${GITHUB_SHA::8}-${{ github.event_name }}" echo ::set-output name=docker_username::${DOCKER_USERNAME} echo ::set-output name=docker_image::${DOCKER_IMAGE} @@ -110,10 +110,10 @@ jobs: - name: Create Manifests run: | - DOCKER_IMAGES="${DOCKER_IMAGE}:linux-amd64-${GITHUB_SHA::8}" - DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm64-${GITHUB_SHA::8}" - DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm-v7-${GITHUB_SHA::8}" - DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm-v6-${GITHUB_SHA::8}" + DOCKER_IMAGES="${DOCKER_IMAGE}:linux-amd64-${GITHUB_SHA::8}-${{ github.event_name }}" + DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm64-${GITHUB_SHA::8}-${{ github.event_name }}" + DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm-v7-${GITHUB_SHA::8}-${{ github.event_name }}" + DOCKER_IMAGES="${DOCKER_IMAGES} ${DOCKER_IMAGE}:linux-arm-v6-${GITHUB_SHA::8}-${{ github.event_name }}" VERSION=edge if [[ $GITHUB_REF == refs/tags/* ]]; then @@ -144,7 +144,7 @@ jobs: PLATFORMS=("amd64" "arm64" "arm-v7" "arm-v6") for PLATFORM in ${PLATFORMS[@]}; do - TAG="linux-${PLATFORM}-${GITHUB_SHA::8}" + TAG="linux-${PLATFORM}-${GITHUB_SHA::8}-${{ github.event_name }}" echo "Deleting tag: ${DOCKER_IMAGE}:${TAG}" curl -X DELETE \ @@ -152,3 +152,8 @@ jobs: -H "Authorization: JWT ${ACCESS_TOKEN}" \ https://hub.docker.com/v2/repositories/${DOCKER_IMAGE}/tags/${TAG}/ done + + - name: Clear + if: always() + run: | + rm -f ${HOME}/.docker/config.json From ef50d7f23b7608909cec7ffa4406e3f362d61e07 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 4 Apr 2021 12:06:12 +0200 Subject: [PATCH 08/28] Add babel to esbuild and start on translation --- ui/babel.config.js | 16 -------- ui/babel.config.json | 4 ++ ui/{build.js => build.mjs} | 23 +++++++---- ui/package-lock.json | 84 ++++++++++++++++++++++++++++++++++++++ ui/package.json | 7 +++- ui/src/Layout.js | 15 ++++--- ui/src/index.js | 18 ++++++++ 7 files changed, 136 insertions(+), 31 deletions(-) delete mode 100644 ui/babel.config.js create mode 100644 ui/babel.config.json rename ui/{build.js => build.mjs} (76%) diff --git a/ui/babel.config.js b/ui/babel.config.js deleted file mode 100644 index 5bc4876..0000000 --- a/ui/babel.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - presets: ['@babel/preset-env', '@babel/preset-react'], - plugins: [ - 'styled-components', - '@babel/plugin-transform-runtime', - '@babel/plugin-transform-modules-commonjs', - 'graphql-tag', - // [ - // 'transform-semantic-ui-react-imports', - // { - // convertMemberImports: true, - // addCssImports: true, - // }, - // ], - ], -} diff --git a/ui/babel.config.json b/ui/babel.config.json new file mode 100644 index 0000000..d2f9ebf --- /dev/null +++ b/ui/babel.config.json @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-react"], + "plugins": ["styled-components", "graphql-tag"] +} diff --git a/ui/build.js b/ui/build.mjs similarity index 76% rename from ui/build.js rename to ui/build.mjs index 3cbaff6..fc31da1 100644 --- a/ui/build.js +++ b/ui/build.mjs @@ -1,9 +1,13 @@ -const fs = require('fs-extra') -const esbuild = require('esbuild') -const bs = require('browser-sync').create() -const historyApiFallback = require('connect-history-api-fallback') +import fs from 'fs-extra' +import esbuild from 'esbuild' +import babel from 'esbuild-plugin-babel' +import browserSync from 'browser-sync' +import historyApiFallback from 'connect-history-api-fallback' +import dotenv from 'dotenv' +import workboxBuild from 'workbox-build' -require('dotenv').config() +dotenv.config() +const bs = browserSync.create() const production = process.env.NODE_ENV == 'production' const watchMode = process.argv[2] == 'watch' @@ -17,6 +21,11 @@ const defineEnv = ENVIRONMENT_VARIABLES.reduce((acc, key) => { const esbuildOptions = { entryPoints: ['src/index.js'], + plugins: [ + babel({ + filter: /photoview\/ui\/src\/.*\.js$/, + }), + ], publicPath: process.env.UI_PUBLIC_URL || '/', outdir: 'dist', format: 'esm', @@ -63,9 +72,9 @@ if (watchMode) { bs.reload(args) }) } else { - esbuild.buildSync(esbuildOptions) + esbuild.build(esbuildOptions).then(() => console.log('esbuild done')) - require('workbox-build').generateSW({ + workboxBuild.generateSW({ globDirectory: 'dist/', globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'], swDest: 'dist/service-worker.js', diff --git a/ui/package-lock.json b/ui/package-lock.json index c5a68b7..37f2286 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -25,6 +25,7 @@ "copy-to-clipboard": "^3.3.1", "dotenv": "^8.2.0", "esbuild": "^0.8.52", + "esbuild-plugin-babel": "^0.2.3", "eslint": "^7.23.0", "eslint-plugin-jest": "^24.3.3", "eslint-plugin-jest-dom": "^3.7.0", @@ -32,11 +33,13 @@ "eslint-plugin-react-hooks": "^4.2.0", "fs-extra": "^9.1.0", "graphql": "^15.5.0", + "i18next": "^20.1.0", "mapbox-gl": "^2.2.0", "prop-types": "^15.7.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", + "react-i18next": "^11.8.12", "react-router-dom": "^5.2.0", "react-router-prop-types": "^1.0.5", "react-spring": "^8.0.27", @@ -4984,6 +4987,14 @@ "esbuild": "bin/esbuild" } }, + "node_modules/esbuild-plugin-babel": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/esbuild-plugin-babel/-/esbuild-plugin-babel-0.2.3.tgz", + "integrity": "sha512-hGLL31n+GvBhkHUpPCt1sU4ynzOH7I1IUkKhera66jigi4mHFPL6dfJo44L6/1rfcZudXx+wGdf9VOifzDPqYQ==", + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -6473,6 +6484,14 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-parse-stringify2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz", + "integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=", + "dependencies": { + "void-elements": "^2.0.1" + } + }, "node_modules/http-errors": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", @@ -6551,6 +6570,14 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/i18next": { + "version": "20.1.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.1.0.tgz", + "integrity": "sha512-sV+ZwTM4Ik4d6wKdwNS/ocKmvXi6DFA/YHMgdQX3i4L5993jnbo1/j1pK/c4+zBOjexer4dt+c5JHsFj4CUoXQ==", + "dependencies": { + "@babel/runtime": "^7.12.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -10909,6 +10936,19 @@ "react-side-effect": "^2.1.0" } }, + "node_modules/react-i18next": { + "version": "11.8.12", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.8.12.tgz", + "integrity": "sha512-M2PSVP9MzT/7yofXfCOF5gAVotinrM4BXWiguk8uFSznJsfFzTjrp3K9CBWcXitpoCBVZGZJ2AnbaWGSNkJqfw==", + "dependencies": { + "@babel/runtime": "^7.13.6", + "html-parse-stringify2": "^2.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -13336,6 +13376,14 @@ "extsprintf": "^1.2.0" } }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vt-pbf": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.1.tgz", @@ -17813,6 +17861,12 @@ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.8.52.tgz", "integrity": "sha512-b5KzFweLLXoXQwdC/e2+Z80c8uo2M5MgP7yQEEebkFw6In4T9CvYcNoM2ElvJt8ByO04zAZUV0fZkXmXoi2s9A==" }, + "esbuild-plugin-babel": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/esbuild-plugin-babel/-/esbuild-plugin-babel-0.2.3.tgz", + "integrity": "sha512-hGLL31n+GvBhkHUpPCt1sU4ynzOH7I1IUkKhera66jigi4mHFPL6dfJo44L6/1rfcZudXx+wGdf9VOifzDPqYQ==", + "requires": {} + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -18955,6 +19009,14 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-parse-stringify2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz", + "integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=", + "requires": { + "void-elements": "^2.0.1" + } + }, "http-errors": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", @@ -19014,6 +19076,14 @@ "integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==", "dev": true }, + "i18next": { + "version": "20.1.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.1.0.tgz", + "integrity": "sha512-sV+ZwTM4Ik4d6wKdwNS/ocKmvXi6DFA/YHMgdQX3i4L5993jnbo1/j1pK/c4+zBOjexer4dt+c5JHsFj4CUoXQ==", + "requires": { + "@babel/runtime": "^7.12.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -22431,6 +22501,15 @@ "react-side-effect": "^2.1.0" } }, + "react-i18next": { + "version": "11.8.12", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.8.12.tgz", + "integrity": "sha512-M2PSVP9MzT/7yofXfCOF5gAVotinrM4BXWiguk8uFSznJsfFzTjrp3K9CBWcXitpoCBVZGZJ2AnbaWGSNkJqfw==", + "requires": { + "@babel/runtime": "^7.13.6", + "html-parse-stringify2": "^2.0.1" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -24430,6 +24509,11 @@ "extsprintf": "^1.2.0" } }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, "vt-pbf": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.1.tgz", diff --git a/ui/package.json b/ui/package.json index db45681..f61677e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -25,6 +25,7 @@ "copy-to-clipboard": "^3.3.1", "dotenv": "^8.2.0", "esbuild": "^0.8.52", + "esbuild-plugin-babel": "^0.2.3", "eslint": "^7.23.0", "eslint-plugin-jest": "^24.3.3", "eslint-plugin-jest-dom": "^3.7.0", @@ -32,11 +33,13 @@ "eslint-plugin-react-hooks": "^4.2.0", "fs-extra": "^9.1.0", "graphql": "^15.5.0", + "i18next": "^20.1.0", "mapbox-gl": "^2.2.0", "prop-types": "^15.7.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", + "react-i18next": "^11.8.12", "react-router-dom": "^5.2.0", "react-router-prop-types": "^1.0.5", "react-spring": "^8.0.27", @@ -49,8 +52,8 @@ "workbox-build": "^6.1.2" }, "scripts": { - "start": "node build.js watch", - "build": "NODE_ENV=production node build.js", + "start": "node build.mjs watch", + "build": "NODE_ENV=production node build.mjs", "test": "npm run lint && npm run jest", "lint": "eslint ./src --max-warnings 0 --cache", "jest": "jest", diff --git a/ui/src/Layout.js b/ui/src/Layout.js index 49e731b..d64dfb0 100644 --- a/ui/src/Layout.js +++ b/ui/src/Layout.js @@ -9,6 +9,7 @@ import { Authorized } from './components/routes/AuthorizedRoute' import { Helmet } from 'react-helmet' import Header from './components/header/Header' import { authToken } from './helpers/authentication' +import { useTranslation } from 'react-i18next' export const ADMIN_QUERY = gql` query adminQuery { @@ -95,6 +96,8 @@ const SideButtonLabel = styled.div` ` export const SideMenu = () => { + const { t } = useTranslation() + const adminQuery = authToken() ? useQuery(ADMIN_QUERY) : null const mapboxQuery = authToken() ? useQuery(MAPBOX_QUERY) : null @@ -105,31 +108,31 @@ export const SideMenu = () => { - Photos + {t('Photos')} - Albums + {t('Albums')} {mapboxEnabled ? ( - Places + {t('Places')} ) : null} - People + {t('People')} {isAdmin ? ( - Settings + {t('Settings')} ) : null} - Log out + {t('Log out')} ) diff --git a/ui/src/index.js b/ui/src/index.js index 9def452..5968284 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -7,6 +7,24 @@ import registerServiceWorker from './registerServiceWorker' import client from './apolloClient' import { ApolloProvider } from '@apollo/client' import { BrowserRouter as Router } from 'react-router-dom' +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' + +i18n.use(initReactI18next).init({ + resources: { + en: { + translation: { + 'Welcome to React': 'Welcome to React and react-i18next', + }, + }, + }, + lng: 'en', + fallbackLng: 'en', + + interpolation: { + escapeValue: false, + }, +}) const Main = () => ( From 84cb8c3c09d409fd8fb5e2adc42e6d74304c8777 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 4 Apr 2021 23:40:10 +0200 Subject: [PATCH 09/28] Configure 18next-extract and start on translation --- ui/babel.config.json | 13 ++- ui/extractedTranslations/da/translation.json | 66 +++++++++++++ ui/extractedTranslations/en/translation.json | 66 +++++++++++++ ui/package-lock.json | 95 +++++++++++++++++++ ui/package.json | 1 + ui/src/Layout.js | 14 +-- ui/src/Pages/AlbumPage/AlbumPage.js | 11 ++- ui/src/Pages/AllAlbumsPage/AlbumsPage.js | 4 +- ui/src/Pages/LoginPage/InitialSetupPage.js | 22 +++-- ui/src/Pages/LoginPage/LoginPage.js | 33 +++++-- ui/src/Pages/SettingsPage/PeriodicScanner.js | 32 +++++-- .../SettingsPage/ScannerConcurrentWorkers.js | 12 ++- ui/src/Pages/SettingsPage/ScannerSection.js | 13 ++- ui/src/Pages/SettingsPage/SettingsPage.js | 5 +- ui/src/index.js | 7 ++ 15 files changed, 354 insertions(+), 40 deletions(-) create mode 100644 ui/extractedTranslations/da/translation.json create mode 100644 ui/extractedTranslations/en/translation.json diff --git a/ui/babel.config.json b/ui/babel.config.json index d2f9ebf..91cd094 100644 --- a/ui/babel.config.json +++ b/ui/babel.config.json @@ -1,4 +1,15 @@ { "presets": ["@babel/preset-react"], - "plugins": ["styled-components", "graphql-tag"] + "plugins": [ + "styled-components", + "graphql-tag", + [ + "i18next-extract", + { + "locales": ["en", "da"], + "discardOldKeys": true, + "defaultValue": null + } + ] + ] } diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json new file mode 100644 index 0000000..936bb0c --- /dev/null +++ b/ui/extractedTranslations/da/translation.json @@ -0,0 +1,66 @@ +{ + "albums_page": { + "title": "Album" + }, + "loading": { + "paginate": { + "media": "Loader flere medier" + } + }, + "login_page": { + "field": { + "password": "Adgangskode", + "submit": "Log ind", + "username": "Brugernavn" + }, + "initial_setup": { + "field": { + "photo_path": "Billedesti" + }, + "field-photo_path": { + "placeholder": "/sti/til/billeder" + }, + "field-submit": "Opsæt Photoview", + "title": "Førstegangsopsætning" + }, + "welcome": "Velkommen til Photoview" + }, + "settings": { + "concurrent_workers": { + "description": "Det maksimale antal medier som må skannes samtidig", + "title": "Samtidige scanner-arbejdere" + }, + "periodic_scanner": { + "checkbox_label": "Aktiver periodiske scanner", + "field": { + "description": "Hvor ofte scanneren bør udføre automatiske scanninger af alle brugere", + "label": "Periodiske scanningsintervaller" + }, + "interval_unit": { + "days": "Dage", + "hour": "Timer", + "minutes": "Minutter", + "months": "Måneder", + "seconds": "Sekunder" + }, + "title": "Periodisk scanner" + }, + "scanner": { + "description": "Vil scanne alle brugere for nye eller opdaterede medier", + "scan_all_users": "Scan alle brugere", + "title": "Scanner" + } + }, + "sidemenu": { + "albums": null, + "logout": "Log af", + "people": "Personer", + "photos": "Billeder", + "places": "Kort", + "settings": "Indstillinger" + }, + "title": { + "loading_album": "Loader album", + "settings": "Indstillinger" + } +} diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json new file mode 100644 index 0000000..02cd8b1 --- /dev/null +++ b/ui/extractedTranslations/en/translation.json @@ -0,0 +1,66 @@ +{ + "albums_page": { + "title": "Albums" + }, + "loading": { + "paginate": { + "media": "Loading more media" + } + }, + "login_page": { + "field": { + "password": "Password", + "submit": "Sign in", + "username": "Username" + }, + "initial_setup": { + "field": { + "photo_path": "Photo path" + }, + "field-photo_path": { + "placeholder": "/path/to/photos" + }, + "field-submit": "Setup Photoview", + "title": "Initial Setup" + }, + "welcome": "Welcome to Photoview" + }, + "settings": { + "concurrent_workers": { + "description": "The maximum amount of scanner jobs that is allowed to run at once", + "title": "Scanner concurrent workers" + }, + "periodic_scanner": { + "checkbox_label": "Enable periodic scanner", + "field": { + "description": "How often the scanner should perform automatic scans of all users", + "label": "Periodic scan interval" + }, + "interval_unit": { + "days": "Days", + "hour": "Hour", + "minutes": "Minutes", + "months": "Months", + "seconds": "Seconds" + }, + "title": "Periodic scanner" + }, + "scanner": { + "description": "Will scan all users for new or updated media", + "scan_all_users": "Scan all users", + "title": "Scanner" + } + }, + "sidemenu": { + "albums": "Albums", + "logout": "Log out", + "people": "People", + "photos": "Photos", + "places": "Places", + "settings": "Settings" + }, + "title": { + "loading_album": "Loading album", + "settings": "Settings" + } +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 37f2286..67884c0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -18,6 +18,7 @@ "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "babel-plugin-graphql-tag": "^3.2.0", + "babel-plugin-i18next-extract": "^0.8.3", "babel-plugin-styled-components": "^1.12.0", "babel-plugin-transform-semantic-ui-react-imports": "^1.4.1", "browser-sync": "^2.26.14", @@ -3413,6 +3414,39 @@ "graphql-tag": "^2.10.1" } }, + "node_modules/babel-plugin-i18next-extract": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-i18next-extract/-/babel-plugin-i18next-extract-0.8.3.tgz", + "integrity": "sha512-ZBhGjP2nLF3pGJO/X6s8DlyUo8zkuPQ09sGZK4XGqtJit/ccj8zocO5JI/F+oFZgKVH1tN8pAQT4fm0JWk2SIQ==", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/types": "7.9.6", + "deepmerge": "^4.2.2", + "i18next": "^19.8.3", + "json-stable-stringify": "^1.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/babel-plugin-i18next-extract/node_modules/@babel/types": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", + "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.9.5", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/babel-plugin-i18next-extract/node_modules/i18next": { + "version": "19.9.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz", + "integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==", + "dependencies": { + "@babel/runtime": "^7.12.0" + } + }, "node_modules/babel-plugin-istanbul": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", @@ -9161,6 +9195,14 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dependencies": { + "jsonify": "~0.0.0" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -9191,6 +9233,14 @@ "node": ">= 10.0.0" } }, + "node_modules/jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "engines": { + "node": "*" + } + }, "node_modules/jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -16576,6 +16626,38 @@ "debug": "^4.1.1" } }, + "babel-plugin-i18next-extract": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-i18next-extract/-/babel-plugin-i18next-extract-0.8.3.tgz", + "integrity": "sha512-ZBhGjP2nLF3pGJO/X6s8DlyUo8zkuPQ09sGZK4XGqtJit/ccj8zocO5JI/F+oFZgKVH1tN8pAQT4fm0JWk2SIQ==", + "requires": { + "@babel/core": "^7.12.3", + "@babel/types": "7.9.6", + "deepmerge": "^4.2.2", + "i18next": "^19.8.3", + "json-stable-stringify": "^1.0.1" + }, + "dependencies": { + "@babel/types": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", + "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "requires": { + "@babel/helper-validator-identifier": "^7.9.5", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "i18next": { + "version": "19.9.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz", + "integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==", + "requires": { + "@babel/runtime": "^7.12.0" + } + } + } + }, "babel-plugin-istanbul": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", @@ -21096,6 +21178,14 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "~0.0.0" + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -21123,6 +21213,11 @@ } } }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", diff --git a/ui/package.json b/ui/package.json index f61677e..68d1e70 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,6 +18,7 @@ "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "babel-plugin-graphql-tag": "^3.2.0", + "babel-plugin-i18next-extract": "^0.8.3", "babel-plugin-styled-components": "^1.12.0", "babel-plugin-transform-semantic-ui-react-imports": "^1.4.1", "browser-sync": "^2.26.14", diff --git a/ui/src/Layout.js b/ui/src/Layout.js index d64dfb0..1a1799c 100644 --- a/ui/src/Layout.js +++ b/ui/src/Layout.js @@ -108,31 +108,33 @@ export const SideMenu = () => { - {t('Photos')} + {t('sidemenu.photos', 'Photos')} - {t('Albums')} + {t('sidemenu.albums', 'Albums')} {mapboxEnabled ? ( - {t('Places')} + {t('sidemenu.places', 'Places')} ) : null} - {t('People')} + {t('sidemenu.people', 'People')} {isAdmin ? ( - {t('Settings')} + + {t('sidemenu.settings', 'Settings')} + ) : null} - {t('Log out')} + {t('sidemenu.logout', 'Log out')} ) diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.js index bd36767..f9b5180 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.js +++ b/ui/src/Pages/AlbumPage/AlbumPage.js @@ -8,6 +8,7 @@ import useURLParameters from '../../hooks/useURLParameters' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../../components/PaginateLoader' import LazyLoad from '../../helpers/LazyLoad' +import { useTranslation } from 'react-i18next' const albumQuery = gql` query albumQuery( @@ -63,6 +64,8 @@ let refetchNeededFavorites = false function AlbumPage({ match }) { const albumId = match.params.id + const { t } = useTranslation() + const { getParam, setParam, setParams } = useURLParameters() const onlyFavorites = getParam('favorites') == '1' ? true : false @@ -139,7 +142,11 @@ function AlbumPage({ match }) { if (error) return
Error
return ( - + ) diff --git a/ui/src/Pages/AllAlbumsPage/AlbumsPage.js b/ui/src/Pages/AllAlbumsPage/AlbumsPage.js index 0751e06..ae805b1 100644 --- a/ui/src/Pages/AllAlbumsPage/AlbumsPage.js +++ b/ui/src/Pages/AllAlbumsPage/AlbumsPage.js @@ -3,6 +3,7 @@ import AlbumBoxes from '../../components/albumGallery/AlbumBoxes' import Layout from '../../Layout' import { useQuery, gql } from '@apollo/client' import LazyLoad from '../../helpers/LazyLoad' +import { useTranslation } from 'react-i18next' const getAlbumsQuery = gql` query getMyAlbums { @@ -19,6 +20,7 @@ const getAlbumsQuery = gql` ` const AlbumsPage = () => { + const { t } = useTranslation() const { loading, error, data } = useQuery(getAlbumsQuery) useEffect(() => { @@ -31,7 +33,7 @@ const AlbumsPage = () => { return ( -

Albums

+

{t('albums_page.title', 'Albums')}

{!loading && ( { + const { t } = useTranslation() + const [state, setState] = useState({ username: '', password: '', @@ -85,7 +88,7 @@ const InitialSetupPage = () => { {initialSetupRedirect}
- Initial Setup + {t('login_page.initial_setup.title', 'Initial Setup')}
{ } > - + handleChange(e, 'username')} /> - + handleChange(e, 'password')} /> - + handleChange(e, 'rootPath')} /> - +
diff --git a/ui/src/Pages/LoginPage/LoginPage.js b/ui/src/Pages/LoginPage/LoginPage.js index ff4a984..753fd74 100644 --- a/ui/src/Pages/LoginPage/LoginPage.js +++ b/ui/src/Pages/LoginPage/LoginPage.js @@ -7,6 +7,7 @@ import { checkInitialSetupQuery, login, Container } from './loginUtilities' import { authToken } from '../../helpers/authentication' import logoPath from '../../assets/photoview-logo.svg' +import { useTranslation } from 'react-i18next' const authorizeMutation = gql` mutation Authorize($username: String!, $password: String!) { @@ -22,18 +23,26 @@ const StyledLogo = styled.img` max-height: 128px; ` -const LogoHeader = props => ( -
- -

Welcome to Photoview

-
-) +const LogoHeader = props => { + const { t } = useTranslation() + + return ( +
+ +

+ {t('login_page.welcome', 'Welcome to Photoview')} +

+
+ ) +} const LogoHeaderStyled = styled(LogoHeader)` margin-bottom: 72px !important; ` const LoginPage = () => { + const { t } = useTranslation() + const [credentials, setCredentials] = useState({ username: '', password: '', @@ -99,14 +108,18 @@ const LoginPage = () => { loading={loading || (data && data.authorizeUser.success)} > - + handleChange(e, 'username')} /> - + { /> - + diff --git a/ui/src/Pages/SettingsPage/PeriodicScanner.js b/ui/src/Pages/SettingsPage/PeriodicScanner.js index 2235eb9..d622890 100644 --- a/ui/src/Pages/SettingsPage/PeriodicScanner.js +++ b/ui/src/Pages/SettingsPage/PeriodicScanner.js @@ -3,6 +3,7 @@ import React, { useRef, useState } from 'react' import { useMutation, useQuery } from '@apollo/client' import { Checkbox, Dropdown, Input, Loader } from 'semantic-ui-react' import { InputLabelDescription, InputLabelTitle } from './SettingsPage' +import { useTranslation } from 'react-i18next' const SCAN_INTERVAL_QUERY = gql` query scanIntervalQuery { @@ -74,6 +75,8 @@ const convertToAppropriateUnit = ({ value, unit }) => { } const PeriodicScanner = () => { + const { t } = useTranslation() + const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false) const [scanInterval, setScanInterval] = useState({ value: 4, @@ -131,38 +134,41 @@ const PeriodicScanner = () => { const scanIntervalUnits = [ { key: 'second', - text: 'Seconds', + text: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'), value: 'second', }, { key: 'minute', - text: 'Minutes', + text: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'), value: 'minute', }, { key: 'hour', - text: 'Hours', + text: t('settings.periodic_scanner.interval_unit.hour', 'Hour'), value: 'hour', }, { key: 'day', - text: 'Days', + text: t('settings.periodic_scanner.interval_unit.days', 'Days'), value: 'day', }, { key: 'month', - text: 'Months', + text: t('settings.periodic_scanner.interval_unit.months', 'Months'), value: 'month', }, ] return ( <> -

Periodic scanner

+

{t('settings.periodic_scanner.title', 'Periodic scanner')}

onScanIntervalCheckboxChange(checked)} @@ -172,9 +178,17 @@ const PeriodicScanner = () => { {enablePeriodicScanner && ( <> { + const { t } = useTranslation() + const workerAmountQuery = useQuery(CONCURRENT_WORKERS_QUERY, { onCompleted(data) { setWorkerAmount(data.siteInfo.concurrentWorkers) @@ -46,9 +49,14 @@ const ScannerConcurrentWorkers = () => { return (
{ + const { t } = useTranslation() const [startScanner, { called }] = useMutation(SCAN_MUTATION) return (
- Scanner + + {t('settings.scanner.title', 'Scanner')} + - Will scan all users for new or updated media + {t( + 'settings.scanner.description', + 'Will scan all users for new or updated media' + )} diff --git a/ui/src/Pages/SettingsPage/SettingsPage.js b/ui/src/Pages/SettingsPage/SettingsPage.js index 04e4334..4d832aa 100644 --- a/ui/src/Pages/SettingsPage/SettingsPage.js +++ b/ui/src/Pages/SettingsPage/SettingsPage.js @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import styled from 'styled-components' import Layout from '../../Layout' @@ -24,8 +25,10 @@ export const InputLabelDescription = styled.p` ` const SettingsPage = () => { + const { t } = useTranslation() + return ( - + diff --git a/ui/src/index.js b/ui/src/index.js index 5968284..eef8ddd 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -20,12 +20,19 @@ i18n.use(initReactI18next).init({ }, lng: 'en', fallbackLng: 'en', + returnNull: false, interpolation: { escapeValue: false, }, }) +import('../extractedTranslations/da/translation.json').then(danish => { + i18n.addResourceBundle('da', 'translation', danish) + console.log('loaded danish') + i18n.changeLanguage('da') +}) + const Main = () => ( From 12df24f366503ec4c20e4a9b9e021c9bc0962d98 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 5 Apr 2021 11:21:58 +0200 Subject: [PATCH 10/28] Translate settings page to danish --- ui/extractedTranslations/da/translation.json | 57 +++++++++++++++++-- ui/extractedTranslations/en/translation.json | 55 ++++++++++++++++-- ui/src/Pages/LoginPage/InitialSetupPage.js | 9 ++- ui/src/Pages/SettingsPage/Users/AddUserRow.js | 13 +++-- .../Pages/SettingsPage/Users/EditUserRow.js | 6 +- .../Users/EditUserRowRootPaths.js | 7 ++- .../SettingsPage/Users/UserChangePassword.js | 25 ++++++-- ui/src/Pages/SettingsPage/Users/UsersTable.js | 22 +++++-- .../Pages/SettingsPage/Users/ViewUserRow.js | 42 ++++++++++---- 9 files changed, 190 insertions(+), 46 deletions(-) diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index 936bb0c..884dfaa 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -2,6 +2,14 @@ "albums_page": { "title": "Album" }, + "general": { + "action": { + "add": "Tilføj", + "cancel": "Annuller", + "remove": "Slet", + "save": "Gem" + } + }, "loading": { "paginate": { "media": "Loader flere medier" @@ -15,12 +23,12 @@ }, "initial_setup": { "field": { - "photo_path": "Billedesti" + "photo_path": { + "label": "Billedesti", + "placeholder": "/sti/til/billeder" + }, + "submit": "Opsæt Photoview" }, - "field-photo_path": { - "placeholder": "/sti/til/billeder" - }, - "field-submit": "Opsæt Photoview", "title": "Førstegangsopsætning" }, "welcome": "Velkommen til Photoview" @@ -49,10 +57,47 @@ "description": "Vil scanne alle brugere for nye eller opdaterede medier", "scan_all_users": "Scan alle brugere", "title": "Scanner" + }, + "users": { + "add_user": { + "submit": "Tilføj bruger" + }, + "confirm_delete_user": { + "action": "Slet {user}", + "description": "<0>Er du sikker på at du vil slette <1>?

Denne handling kan ikke fortrydes

", + "title": "Slet bruger" + }, + "password_reset": { + "description": "Ændre adgangskode for <1>", + "form": { + "label": "Ny adgangskode", + "placeholder": "adgangskode", + "submit": "Ændre adgangskode" + }, + "title": "Ændre adgangskode" + }, + "table": { + "column_names": { + "action": "Handling", + "admin": "Admin", + "photo_path": "Billedesti", + "username": "Brugernavn" + }, + "new_user": "Ny bruger", + "row": { + "action": { + "change_password": "Ændre adgangskode", + "delete": "Slet", + "edit": "Rediger", + "scan": "Scan" + } + } + }, + "title": "Brugere" } }, "sidemenu": { - "albums": null, + "albums": "Albums", "logout": "Log af", "people": "Personer", "photos": "Billeder", diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index 02cd8b1..9f3c14a 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -2,6 +2,14 @@ "albums_page": { "title": "Albums" }, + "general": { + "action": { + "add": "Add", + "cancel": "Cancel", + "remove": "Remove", + "save": "Save" + } + }, "loading": { "paginate": { "media": "Loading more media" @@ -15,12 +23,12 @@ }, "initial_setup": { "field": { - "photo_path": "Photo path" + "photo_path": { + "label": "Photo path", + "placeholder": "/path/to/photos" + }, + "submit": "Setup Photoview" }, - "field-photo_path": { - "placeholder": "/path/to/photos" - }, - "field-submit": "Setup Photoview", "title": "Initial Setup" }, "welcome": "Welcome to Photoview" @@ -49,6 +57,43 @@ "description": "Will scan all users for new or updated media", "scan_all_users": "Scan all users", "title": "Scanner" + }, + "users": { + "add_user": { + "submit": "Add user" + }, + "confirm_delete_user": { + "action": "Delete {user}", + "description": "<0>Are you sure, you want to delete <1>?

This action cannot be undone

", + "title": "Delete user" + }, + "password_reset": { + "description": "Change password for <1>", + "form": { + "label": "New password", + "placeholder": "password", + "submit": "Change password" + }, + "title": "Change password" + }, + "table": { + "column_names": { + "action": "Action", + "admin": "Admin", + "photo_path": "Photo path", + "username": "Username" + }, + "new_user": "New user", + "row": { + "action": { + "change_password": "Change password", + "delete": "Delete", + "edit": "Edit", + "scan": "Scan" + } + } + }, + "title": "Users" } }, "sidemenu": { diff --git a/ui/src/Pages/LoginPage/InitialSetupPage.js b/ui/src/Pages/LoginPage/InitialSetupPage.js index f42c0d6..4be5b51 100644 --- a/ui/src/Pages/LoginPage/InitialSetupPage.js +++ b/ui/src/Pages/LoginPage/InitialSetupPage.js @@ -111,11 +111,14 @@ const InitialSetupPage = () => { { diff --git a/ui/src/Pages/SettingsPage/Users/AddUserRow.js b/ui/src/Pages/SettingsPage/Users/AddUserRow.js index 9a4c037..3d87f44 100644 --- a/ui/src/Pages/SettingsPage/Users/AddUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/AddUserRow.js @@ -1,6 +1,7 @@ import { gql, useMutation } from '@apollo/client' import PropTypes from 'prop-types' import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Button, Checkbox, Input, Table } from 'semantic-ui-react' const createUserMutation = gql` @@ -30,6 +31,7 @@ const initialState = { } const AddUserRow = ({ setShow, show, onUserAdded }) => { + const { t } = useTranslation() const [state, setState] = useState(initialState) const [addRootPath, { loading: addRootPathLoading }] = useMutation( @@ -81,14 +83,17 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { updateInput(e, 'username')} /> updateInput(e, 'rootPath')} /> @@ -108,7 +113,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRow.js b/ui/src/Pages/SettingsPage/Users/EditUserRow.js index 881eb3a..3c5d9f2 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRow.js @@ -1,4 +1,5 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import { Button, Checkbox, Input, Table } from 'semantic-ui-react' import { EditRootPaths } from './EditUserRowRootPaths' import { UserRowProps } from './UserRow' @@ -10,6 +11,7 @@ const EditUserRow = ({ updateUser, updateUserLoading, }) => { + const { t } = useTranslation() function updateInput(event, key) { setState(state => ({ ...state, @@ -52,7 +54,7 @@ const EditUserRow = ({ })) } > - Cancel + {t('general.action.cancel', 'Cancel')} diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js index f5f0ff8..11e84bc 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js @@ -4,6 +4,7 @@ import { gql, useMutation } from '@apollo/client' import { Button, Icon, Input } from 'semantic-ui-react' import styled from 'styled-components' import { USERS_QUERY } from './UsersTable' +import { useTranslation } from 'react-i18next' const userAddRootPathMutation = gql` mutation userAddRootPath($id: ID!, $rootPath: String!) { @@ -28,6 +29,7 @@ const RootPathListItem = styled.li` ` const EditRootPath = ({ album, user }) => { + const { t } = useTranslation() const [removeAlbumPath, { loading }] = useMutation( userRemoveAlbumPathMutation, { @@ -55,7 +57,7 @@ const EditRootPath = ({ album, user }) => { } > - Remove + {t('general.action.remove', 'Remove')} ) @@ -72,6 +74,7 @@ const NewRootPathInput = styled(Input)` ` const EditNewRootPath = ({ userID }) => { + const { t } = useTranslation() const [value, setValue] = useState('') const [addRootPath, { loading }] = useMutation(userAddRootPathMutation, { refetchQueries: [ @@ -91,7 +94,7 @@ const EditNewRootPath = ({ userID }) => { action={{ positive: true, icon: 'add', - content: 'Add', + content: t('general.action.add', 'Add'), onClick: () => { setValue('') addRootPath({ diff --git a/ui/src/Pages/SettingsPage/Users/UserChangePassword.js b/ui/src/Pages/SettingsPage/Users/UserChangePassword.js index 39d9b9a..d7a537e 100644 --- a/ui/src/Pages/SettingsPage/Users/UserChangePassword.js +++ b/ui/src/Pages/SettingsPage/Users/UserChangePassword.js @@ -2,6 +2,7 @@ import React, { useState } from 'react' import PropTypes from 'prop-types' import { gql, useMutation } from '@apollo/client' import { Button, Form, Input, Modal } from 'semantic-ui-react' +import { Trans, useTranslation } from 'react-i18next' const changeUserPasswordMutation = gql` mutation changeUserPassword($userId: ID!, $password: String!) { @@ -12,6 +13,7 @@ const changeUserPasswordMutation = gql` ` const ChangePasswordModal = ({ onClose, user, ...props }) => { + const { t } = useTranslation() const [passwordInput, setPasswordInput] = useState('') const [changePassword] = useMutation(changeUserPasswordMutation, { @@ -22,16 +24,25 @@ const ChangePasswordModal = ({ onClose, user, ...props }) => { return ( - Change password + + {t('settings.users.password_reset.title', 'Change password')} +

- Change password for {user.username} + + Change password for {user.username} +

- + setPasswordInput(e.target.value)} type="password" /> @@ -39,7 +50,9 @@ const ChangePasswordModal = ({ onClose, user, ...props }) => {
- +
diff --git a/ui/src/Pages/SettingsPage/Users/UsersTable.js b/ui/src/Pages/SettingsPage/Users/UsersTable.js index 279446b..90b4ca1 100644 --- a/ui/src/Pages/SettingsPage/Users/UsersTable.js +++ b/ui/src/Pages/SettingsPage/Users/UsersTable.js @@ -5,6 +5,7 @@ import { useQuery, gql } from '@apollo/client' import UserRow from './UserRow' import AddUserRow from './AddUserRow' import { SectionTitle } from '../SettingsPage' +import { useTranslation } from 'react-i18next' export const USERS_QUERY = gql` query settingsUsersQuery { @@ -22,6 +23,7 @@ export const USERS_QUERY = gql` ` const UsersTable = () => { + const { t } = useTranslation() const [showAddUser, setShowAddUser] = useState(false) const { loading, error, data, refetch } = useQuery(USERS_QUERY) @@ -39,15 +41,23 @@ const UsersTable = () => { return (
- Users + {t('settings.users.title', 'Users')} - Username - Photo path - Admin - Action + + {t('settings.users.table.column_names.username', 'Username')} + + + {t('settings.users.table.column_names.photo_path', 'Photo path')} + + + {t('settings.users.table.column_names.admin', 'Admin')} + + + {t('settings.users.table.column_names.action', 'Action')} + @@ -73,7 +83,7 @@ const UsersTable = () => { onClick={() => setShowAddUser(true)} > - New user + {t('settings.users.table.new_user', 'New user')} diff --git a/ui/src/Pages/SettingsPage/Users/ViewUserRow.js b/ui/src/Pages/SettingsPage/Users/ViewUserRow.js index 17053fe..b2d6ef3 100644 --- a/ui/src/Pages/SettingsPage/Users/ViewUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/ViewUserRow.js @@ -1,4 +1,5 @@ import React from 'react' +import { Trans, useTranslation } from 'react-i18next' import { Button, Icon, Table, Modal } from 'semantic-ui-react' import styled from 'styled-components' import ChangePasswordModal from './UserChangePassword' @@ -22,6 +23,7 @@ const ViewUserRow = ({ showChangePassword, showConfirmDelete, }) => { + const { t } = useTranslation() const paths = ( {user.rootAlbums.map(album => ( @@ -45,18 +47,21 @@ const ViewUserRow = ({ }} > - Edit + {t('settings.users.table.row.action.edit', 'Edit')} - Delete + {t('settings.users.table.row.action.delete', 'Delete')} - Delete user + + {t('settings.users.confirm_delete_user.title', 'Delete user')} + -

- {`Are you sure, you want to delete `} - {user.username}? -

-

{`This action cannot be undone`}

+ +

+ {`Are you sure, you want to delete `} + {user.username}? +

+

{`This action cannot be undone`}

+
- +
From e530ce5555d798e98a1216a8358d41e65bffc600 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 5 Apr 2021 18:49:12 +0200 Subject: [PATCH 11/28] Add more translations --- ui/extractedTranslations/da/translation.json | 98 +++++++++++- ui/extractedTranslations/en/translation.json | 96 ++++++++++++ ui/src/components/AlbumFilter.js | 75 ++++++---- ui/src/components/header/Searchbar.js | 25 +++- ui/src/components/sidebar/AlbumSidebar.js | 6 +- ui/src/components/sidebar/MediaSidebar.js | 150 ++++++++++++------- ui/src/components/sidebar/Sharing.js | 25 ++-- ui/src/components/sidebar/SidebarDownload.js | 57 +++++-- 8 files changed, 414 insertions(+), 118 deletions(-) diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index 884dfaa..b3d0d40 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -1,4 +1,14 @@ { + "album_filter": { + "only_favorites": "Vis kun favoritter", + "sort_by": "Sorter efter", + "sorting_options": { + "date_imported": "Dato for importering", + "date_shot": "Dato", + "title": "Titel", + "type": "Type" + } + }, "albums_page": { "title": "Album" }, @@ -6,8 +16,24 @@ "action": { "add": "Tilføj", "cancel": "Annuller", - "remove": "Slet", + "delete": "Slet", + "remove": "Fjern", "save": "Gem" + }, + "loading": { + "default": "Loader...", + "shares": null + } + }, + "header": { + "search": { + "loading": "Loader resultater...", + "no_results": "Fandt ingen resultater", + "placeholder": "Søg", + "result_type": { + "albums": "Albums", + "photos": "Billeder" + } } }, "loading": { @@ -96,6 +122,76 @@ "title": "Brugere" } }, + "sidebar": { + "album": { + "title": "Album indstillinger" + }, + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "kilo_byte": "{{count}} KB", + "mega_byte": "{{count}} MB", + "tera_byte": "{{count}} TB" + }, + "table_columns": { + "dimensions": "Dimension", + "file_size": "Størrelse", + "file_type": "Type", + "name": "Navn" + }, + "title": "Download" + }, + "media": { + "exif": { + "exposure_program": { + "action_program": "Actionprogram", + "aperture_priority": "Blændeprioritet", + "bulb": "Bulb", + "creative_program": "Kreativ program", + "landscape_mode": "Landskabsmode", + "manual": "Manuel", + "normal_program": "Normal program", + "not_defined": "Ikke defineret", + "portrait_mode": "Portræt mode", + "shutter_priority": "Lukkerprioritet" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Blitz affyrede ikke", + "fired": "Affyrede", + "no_flash": "Ingen blitz", + "no_flash_function": "Ingen blitz-funktion", + "off": "Slukket", + "on": "Tændt", + "red_eye_reduction": "Røde øjne reduktion", + "return_detected": "Retur registreret", + "return_not_detected": "Retur ikke registreret" + }, + "name": { + "aperture": "Blænde", + "camera": "Kamera", + "date_shot": "Dato", + "exposure": "Lukketid", + "exposure_program": "Lukketid program", + "flash": "Blitz", + "focal_length": "Fokallængde", + "iso": "ISO", + "lens": "Lense", + "maker": "Mærke" + } + } + }, + "sharing": { + "add_share": "Tilføj deling", + "copy_link": "Kopier link", + "no_shares_found": "Ingen delinger fundet", + "public_link": "Offentligt link", + "table_header": "Offentlige delinger", + "title": "Indstillinger for deling" + } + }, "sidemenu": { "albums": "Albums", "logout": "Log af", diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index 9f3c14a..4edcd11 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -1,4 +1,14 @@ { + "album_filter": { + "only_favorites": "Show only favorites", + "sort_by": "Sort by", + "sorting_options": { + "date_imported": "Date imported", + "date_shot": "Date shot", + "title": "Title", + "type": "Kind" + } + }, "albums_page": { "title": "Albums" }, @@ -6,8 +16,24 @@ "action": { "add": "Add", "cancel": "Cancel", + "delete": "Delete", "remove": "Remove", "save": "Save" + }, + "loading": { + "default": "Loading...", + "shares": "Loading shares..." + } + }, + "header": { + "search": { + "loading": "Loading results...", + "no_results": "No results found", + "placeholder": "Search", + "result_type": { + "albums": "Albums", + "photos": "Photos" + } } }, "loading": { @@ -96,6 +122,76 @@ "title": "Users" } }, + "sidebar": { + "album": { + "title": "Album options" + }, + "download": { + "filesize": { + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "kilo_byte": "{{count}} KB", + "mega_byte": "{{count}} MB", + "tera_byte": "{{count}} TB" + }, + "table_columns": { + "dimensions": "Dimensions", + "file_size": "Size", + "file_type": "Type", + "name": "Name" + }, + "title": "Download" + }, + "media": { + "exif": { + "exposure_program": { + "action_program": "Action program", + "aperture_priority": "Aperture priority", + "bulb": "Bulb", + "creative_program": "Creative program", + "landscape_mode": "Landscape mode", + "manual": "Manual", + "normal_program": "Normal program", + "not_defined": "Not defined", + "portrait_mode": "Portrait mode", + "shutter_priority": "Shutter priority" + }, + "flash": { + "auto": "Auto", + "did_not_fire": "Did not fire", + "fired": "Fired", + "no_flash": "No Flash", + "no_flash_function": "No flash function", + "off": "Off", + "on": "On", + "red_eye_reduction": "Red-eye reduction", + "return_detected": "Return detected", + "return_not_detected": "Return not detected" + }, + "name": { + "aperture": "Aperture", + "camera": "Camera", + "date_shot": "Date shot", + "exposure": "Exposure", + "exposure_program": "Program", + "flash": "Flash", + "focal_length": "Focal length", + "iso": "ISO", + "lens": "Lens", + "maker": "Maker" + } + } + }, + "sharing": { + "add_share": "Add shares", + "copy_link": "Copy Link", + "no_shares_found": "No shares found", + "public_link": "Public Link", + "table_header": "Public shares", + "title": "Sharing options" + } + }, "sidemenu": { "albums": "Albums", "logout": "Log out", diff --git a/ui/src/components/AlbumFilter.js b/ui/src/components/AlbumFilter.js index 26c421b..f8a6ba8 100644 --- a/ui/src/components/AlbumFilter.js +++ b/ui/src/components/AlbumFilter.js @@ -3,29 +3,7 @@ import { authToken } from '../helpers/authentication' import { Checkbox, Dropdown, Button, Icon } from 'semantic-ui-react' import styled from 'styled-components' import PropTypes from 'prop-types' - -const sortingOptions = [ - { - key: 'date_shot', - value: 'date_shot', - text: 'Date shot', - }, - { - key: 'updated_at', - value: 'updated_at', - text: 'Date imported', - }, - { - key: 'title', - value: 'title', - text: 'Title', - }, - { - key: 'type', - value: 'type', - text: 'Kind', - }, -] +import { useTranslation } from 'react-i18next' const FavoritesCheckboxStyle = styled(Checkbox)` margin-bottom: 16px; @@ -56,14 +34,18 @@ const FavoritesCheckboxStyle = styled(Checkbox)` } ` -export const FavoritesCheckbox = ({ onlyFavorites, setOnlyFavorites }) => ( - setOnlyFavorites(result.checked)} - /> -) +export const FavoritesCheckbox = ({ onlyFavorites, setOnlyFavorites }) => { + const { t } = useTranslation() + + return ( + setOnlyFavorites(result.checked)} + /> + ) +} FavoritesCheckbox.propTypes = { onlyFavorites: PropTypes.bool.isRequired, @@ -75,17 +57,46 @@ const OrderDirectionButton = styled(Button)` margin-left: 10px !important; ` +const SortByLabel = styled.strong` + margin-left: 4px; + margin-right: 6px; +` + const AlbumFilter = ({ onlyFavorites, setOnlyFavorites, setOrdering, ordering, }) => { + const { t } = useTranslation() const onChangeOrderDirection = (e, data) => { const direction = data.children.props.name === 'arrow up' ? 'DESC' : 'ASC' setOrdering({ orderDirection: direction }) } + const sortingOptions = [ + { + key: 'date_shot', + value: 'date_shot', + text: t('album_filter.sorting_options.date_shot', 'Date shot'), + }, + { + key: 'updated_at', + value: 'updated_at', + text: t('album_filter.sorting_options.date_imported', 'Date imported'), + }, + { + key: 'title', + value: 'title', + text: t('album_filter.sorting_options.title', 'Title'), + }, + { + key: 'type', + value: 'type', + text: t('album_filter.sorting_options.type', 'Kind'), + }, + ] + return ( <> {authToken() && ( @@ -94,7 +105,7 @@ const AlbumFilter = ({ setOnlyFavorites={setOnlyFavorites} /> )} - Sort by + {t('album_filter.sort_by', 'Sort by')} { + const { t } = useTranslation() const [fetchSearches, fetchResult] = useLazyQuery(SEARCH_QUERY) const [query, setQuery] = useState('') const [fetched, setFetched] = useState(false) @@ -111,7 +113,11 @@ const SearchBar = () => { return ( - + {results} ) @@ -123,6 +129,7 @@ const ResultTitle = styled.h1` ` const SearchResults = ({ result }) => { + const { t } = useTranslation() const { data, loading } = result const query = data && data.search.query @@ -130,9 +137,9 @@ const SearchResults = ({ result }) => { const albums = (data && data.search.albums) || [] let message = null - if (loading) message = 'Loading results...' + if (loading) message = t('header.search.loading', 'Loading results...') else if (data && media.length == 0 && albums.length == 0) - message = 'No results found' + message = t('header.search.no_results', 'No results found') const albumElements = albums.map(album => ( @@ -151,9 +158,17 @@ const SearchResults = ({ result }) => { show={data} > {message} - {albumElements.length > 0 && Albums} + {albumElements.length > 0 && ( + + {t('header.search.result_type.albums', 'Albums')} + + )} {albumElements} - {mediaElements.length > 0 && Photos} + {mediaElements.length > 0 && ( + + {t('header.search.result_type.photos', 'Photos')} + + )} {mediaElements} ) diff --git a/ui/src/components/sidebar/AlbumSidebar.js b/ui/src/components/sidebar/AlbumSidebar.js index 0a8ba1b..3fab32b 100644 --- a/ui/src/components/sidebar/AlbumSidebar.js +++ b/ui/src/components/sidebar/AlbumSidebar.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { useQuery, gql } from '@apollo/client' import SidebarShare from './Sharing' +import { useTranslation } from 'react-i18next' const albumQuery = gql` query getAlbumSidebar($id: ID!) { @@ -13,16 +14,17 @@ const albumQuery = gql` ` const AlbumSidebar = ({ albumId }) => { + const { t } = useTranslation() const { loading, error, data } = useQuery(albumQuery, { variables: { id: albumId }, }) - if (loading) return
Loading...
+ if (loading) return
{t('general.loading.default', 'Loading...')}
if (error) return
{error.message}
return (
-

Album options

+

{t('sidebar.album.title', 'Album options')}

{data.album.title}

diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index 8364f40..2954363 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -9,6 +9,7 @@ import SidebarDownload from './SidebarDownload' import SidebarItem from './SidebarItem' import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay' import { isNil } from '../../helpers/utils' +import { useTranslation } from 'react-i18next' const mediaQuery = gql` query sidebarPhoto($id: ID!) { @@ -123,10 +124,13 @@ const MetadataInfoContainer = styled.div` ` export const MetadataInfo = ({ media }) => { + const { t } = useTranslation() let exifItems = [] + const exifName = exifNameLookup(t) + if (media?.exif) { - let exifKeys = Object.keys(exifNameLookup).filter( + let exifKeys = Object.keys(exifName).filter( x => media.exif[x] !== null && x != '__typename' ) @@ -146,6 +150,8 @@ export const MetadataInfo = ({ media }) => { exif.exposure = `1/${1 / exif.exposure}` } + const exposurePrograms = exposureProgramsLookup(t) + if ( !isNil(exif.exposureProgram) && exposurePrograms[exif.exposureProgram] @@ -168,7 +174,7 @@ export const MetadataInfo = ({ media }) => { } exifItems = exifKeys.map(key => ( - + )) } @@ -206,62 +212,98 @@ MetadataInfo.propTypes = { media: PropTypes.object, } -const exifNameLookup = { - camera: 'Camera', - maker: 'Maker', - lens: 'Lens', - exposureProgram: 'Program', - dateShot: 'Date Shot', - exposure: 'Exposure', - aperture: 'Aperture', - iso: 'ISO', - focalLength: 'Focal Length', - flash: 'Flash', -} +const exifNameLookup = t => ({ + camera: t('sidebar.media.exif.name.camera', 'Camera'), + maker: t('sidebar.media.exif.name.maker', 'Maker'), + lens: t('sidebar.media.exif.name.lens', 'Lens'), + exposureProgram: t('sidebar.media.exif.name.exposure_program', 'Program'), + dateShot: t('sidebar.media.exif.name.date_shot', 'Date shot'), + exposure: t('sidebar.media.exif.name.exposure', 'Exposure'), + aperture: t('sidebar.media.exif.name.aperture', 'Aperture'), + iso: t('sidebar.media.exif.name.iso', 'ISO'), + focalLength: t('sidebar.media.exif.name.focal_length', 'Focal length'), + flash: t('sidebar.media.exif.name.flash', 'Flash'), +}) // From https://exiftool.org/TagNames/EXIF.html -const exposurePrograms = { - 0: 'Not defined', - 1: 'Manual', - 2: 'Normal program', - 3: 'Aperture priority', - 4: 'Shutter priority', - 5: 'Creative program', - 6: 'Action program', - 7: 'Portrait mode', - 8: 'Landscape mode ', - 9: 'Bulb', -} +const exposureProgramsLookup = t => ({ + 0: t('sidebar.media.exif.exposure_program.not_defined', 'Not defined'), + 1: t('sidebar.media.exif.exposure_program.manual', 'Manual'), + 2: t('sidebar.media.exif.exposure_program.normal_program', 'Normal program'), + 3: t( + 'sidebar.media.exif.exposure_program.aperture_priority', + 'Aperture priority' + ), + 4: t( + 'sidebar.media.exif.exposure_program.shutter_priority', + 'Shutter priority' + ), + 5: t( + 'sidebar.media.exif.exposure_program.creative_program', + 'Creative program' + ), + 6: t('sidebar.media.exif.exposure_program.action_program', 'Action program'), + 7: t('sidebar.media.exif.exposure_program.portrait_mode', 'Portrait mode'), + 8: t('sidebar.media.exif.exposure_program.landscape_mode', 'Landscape mode'), + 9: t('sidebar.media.exif.exposure_program.bulb', 'Bulb'), +}) // From https://exiftool.org/TagNames/EXIF.html#Flash -const flash = { - 0x0: 'No Flash', - 0x1: 'Fired', - 0x5: 'Fired, Return not detected', - 0x7: 'Fired, Return detected', - 0x8: 'On, Did not fire', - 0x9: 'On, Fired', - 0xd: 'On, Return not detected', - 0xf: 'On, Return detected', - 0x10: 'Off, Did not fire', - 0x14: 'Off, Did not fire, Return not detected', - 0x18: 'Auto, Did not fire', - 0x19: 'Auto, Fired', - 0x1d: 'Auto, Fired, Return not detected', - 0x1f: 'Auto, Fired, Return detected', - 0x20: 'No flash function', - 0x30: 'Off, No flash function', - 0x41: 'Fired, Red-eye reduction', - 0x45: 'Fired, Red-eye reduction, Return not detected', - 0x47: 'Fired, Red-eye reduction, Return detected', - 0x49: 'On, Red-eye reduction', - 0x4d: 'On, Red-eye reduction, Return not detected', - 0x4f: 'On, Red-eye reduction, Return detected', - 0x50: 'Off, Red-eye reduction', - 0x58: 'Auto, Did not fire, Red-eye reduction', - 0x59: 'Auto, Fired, Red-eye reduction', - 0x5d: 'Auto, Fired, Red-eye reduction, Return not detected', - 0x5f: 'Auto, Fired, Red-eye reduction, Return detected', +const flash = t => { + const values = { + no_flash: t('sidebar.media.exif.flash.no_flash', 'No Flash'), + fired: t('sidebar.media.exif.flash.fired', 'Fired'), + did_not_fire: t('sidebar.media.exif.flash.did_not_fire', 'Did not fire'), + on: t('sidebar.media.exif.flash.on', 'On'), + off: t('sidebar.media.exif.flash.off', 'Off'), + auto: t('sidebar.media.exif.flash.auto', 'Auto'), + return_not_detected: t( + 'sidebar.media.exif.flash.return_not_detected', + 'Return not detected' + ), + return_detected: t( + 'sidebar.media.exif.flash.return_detected', + 'Return detected' + ), + no_flash_function: t( + 'sidebar.media.exif.flash.no_flash_function', + 'No flash function' + ), + red_eye_reduction: t( + 'sidebar.media.exif.flash.red_eye_reduction', + 'Red-eye reduction' + ), + } + + return { + 0x0: values['no_flash'], + 0x1: values['fired'], + 0x5: `${values['fired']}, ${values['return_not_detected']}`, + 0x7: `${values['fired']}, ${values['return_detected']}`, + 0x8: `${values['on']}, ${values['did_not_fire']}`, + 0x9: `${values['on']}, ${values['fired']}`, + 0xd: `${values['on']}, ${values['return_not_detected']}`, + 0xf: `${values['on']}, ${values['return_detected']}`, + 0x10: `${values['off']}, ${values['did_not_fire']}`, + 0x14: `${values['off']}, ${values['did_not_fire']}, ${values['return_not_detected']}`, + 0x18: `${values['auto']}, ${values['did_not_fire']}`, + 0x19: `${values['auto']}, ${values['fired']}`, + 0x1d: `${values['auto']}, ${values['fired']}, ${values['return_not_detected']}`, + 0x1f: `${values['auto']}, ${values['fired']}, ${values['return_detected']}`, + 0x20: `${values['no_flash_function']}`, + 0x30: `${values['off']}, ${values['no_flash_function']}`, + 0x41: `${values['fired']}, ${values['red_eye_reduction']}`, + 0x45: `${values['fired']}, ${values['red_eye_reduction']}, ${values['return_not_detected']}`, + 0x47: `${values['fired']}, ${values['red_eye_reduction']}, ${values['return_detected']}`, + 0x49: `${values['on']}, ${values['red_eye_reduction']}`, + 0x4d: `${values['on']}, ${values['red_eye_reduction']}, ${values['return_not_detected']}`, + 0x4f: `${values['on']}, ${values['red_eye_reduction']}, ${values['return_detected']}`, + 0x50: `${values['off']}, ${values['red_eye_reduction']}`, + 0x58: `${values['auto']}, ${values['did_not_fire']}, ${values['red_eye_reduction']}`, + 0x59: `${values['auto']}, ${values['fired']}, ${values['red_eye_reduction']}`, + 0x5d: `${values['auto']}, ${values['red_eye_reduction']}, ${values['return_not_detected']}`, + 0x5f: `${values['auto']}, ${values['red_eye_redcution']}, ${values['return_detected']}`, + } } // From https://exiftool.org/TagNames/EXIF.html diff --git a/ui/src/components/sidebar/Sharing.js b/ui/src/components/sidebar/Sharing.js index 849c7c3..0adae35 100644 --- a/ui/src/components/sidebar/Sharing.js +++ b/ui/src/components/sidebar/Sharing.js @@ -12,6 +12,7 @@ import { import copy from 'copy-to-clipboard' import { authToken } from '../../helpers/authentication' import styled from 'styled-components' +import { useTranslation } from 'react-i18next' const sharePhotoQuery = gql` query sidbarGetPhotoShares($id: ID!) { @@ -73,6 +74,7 @@ const deleteShareMutation = gql` ` const ShareItemMoreDropdown = ({ id, share, isPhoto }) => { + const { t } = useTranslation() const query = isPhoto ? sharePhotoQuery : shareAlbumQuery const [deleteShare, { loading: deleteShareLoading }] = useMutation( @@ -187,7 +189,7 @@ const ShareItemMoreDropdown = ({ id, share, isPhoto }) => { }} > e.stopPropagation()} checked={showPasswordInput} onChange={() => { @@ -197,7 +199,7 @@ const ShareItemMoreDropdown = ({ id, share, isPhoto }) => { {addPasswordInput} { @@ -224,6 +226,7 @@ const ShareButtonGroup = styled(Button.Group)` ` const SidebarShare = ({ photo, album }) => { + const { t } = useTranslation() if ((!photo || !photo.id) && (!album || !album.id)) return null if (!authToken()) return null @@ -257,7 +260,7 @@ const SidebarShare = ({ photo, album }) => { } if (!content && sharesLoading) { - content =
Loading shares...
+ content =
{t('general.loading.shares', 'Loading shares...')}
} if (!content) { @@ -266,13 +269,13 @@ const SidebarShare = ({ photo, album }) => { const optionsRows = shares.map(share => ( - Public Link {share.token} + {t('sidebar.sharing.public_link', 'Public Link')} {share.token}
- Public Shares + + {t('sidebar.sharing.table_header', 'Public shares')} + {optionsRows} @@ -304,7 +311,7 @@ const SidebarShare = ({ photo, album }) => {
- Name - Dimensions - Size - Type + + {t('sidebar.download.table_columns.name', 'Name')} + + + {t('sidebar.download.table_columns.dimensions', 'Dimensions')} + + + {t('sidebar.download.table_columns.file_size', 'Size')} + + + {t('sidebar.download.table_columns.file_type', 'Type')} + {downloadRows} From b05f5b8eb7505f1f1fcd39aa409a68fec51af4c4 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 5 Apr 2021 23:11:48 +0200 Subject: [PATCH 12/28] More translations, fix tests, modify build process --- ui/babel.config.js | 32 ++++++++++ ui/babel.config.json | 15 ----- ui/build.mjs | 20 ++++-- ui/extractedTranslations/da/translation.json | 35 +++++++++-- ui/extractedTranslations/en/translation.json | 37 ++++++++--- ui/package.json | 4 +- ui/src/Layout.test.js | 2 + ui/src/Pages/AlbumPage/AlbumPage.js | 2 +- ui/src/Pages/PeoplePage/PeoplePage.js | 18 ++++-- ui/src/Pages/SharePage/AlbumSharePage.js | 10 ++- ui/src/Pages/SharePage/SharePage.js | 61 +++++++++++++------ ui/src/Pages/SharePage/SharePage.test.js | 2 + .../components/photoGallery/PhotoGallery.js | 6 +- ui/src/components/routes/Routes.js | 11 +++- ui/src/components/routes/Routes.test.js | 2 + ui/src/components/sidebar/MediaSidebar.js | 3 +- .../components/sidebar/MediaSidebar.test.js | 10 +-- .../timelineGallery/TimelineGallery.js | 8 ++- ui/src/index.js | 19 +----- ui/src/localization.js | 21 +++++++ 20 files changed, 228 insertions(+), 90 deletions(-) create mode 100644 ui/babel.config.js delete mode 100644 ui/babel.config.json create mode 100644 ui/src/localization.js diff --git a/ui/babel.config.js b/ui/babel.config.js new file mode 100644 index 0000000..43e0db3 --- /dev/null +++ b/ui/babel.config.js @@ -0,0 +1,32 @@ +module.exports = function (api) { + const isTest = api.env('test') + const isProduction = api.env('NODE_ENV') == 'production' + + let presets = ['@babel/preset-react'] + let plugins = [] + + if (isTest) { + presets.push('@babel/preset-env') + + plugins.push('@babel/plugin-transform-runtime') + plugins.push('@babel/plugin-transform-modules-commonjs') + } else { + plugins.push(['styled-components', { pure: true }]) + plugins.push('graphql-tag') + if (!isProduction) { + plugins.push([ + 'i18next-extract', + { + locales: ['en', 'da'], + discardOldKeys: true, + defaultValue: null, + }, + ]) + } + } + + return { + presets: presets, + plugins: plugins, + } +} diff --git a/ui/babel.config.json b/ui/babel.config.json deleted file mode 100644 index 91cd094..0000000 --- a/ui/babel.config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": ["@babel/preset-react"], - "plugins": [ - "styled-components", - "graphql-tag", - [ - "i18next-extract", - { - "locales": ["en", "da"], - "discardOldKeys": true, - "defaultValue": null - } - ] - ] -} diff --git a/ui/build.mjs b/ui/build.mjs index fc31da1..f6605ad 100644 --- a/ui/build.mjs +++ b/ui/build.mjs @@ -72,11 +72,19 @@ if (watchMode) { bs.reload(args) }) } else { - esbuild.build(esbuildOptions).then(() => console.log('esbuild done')) + const esbuildPromise = esbuild + .build(esbuildOptions) + .then(() => console.log('esbuild done')) - workboxBuild.generateSW({ - globDirectory: 'dist/', - globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'], - swDest: 'dist/service-worker.js', - }) + const workboxPromise = workboxBuild + .generateSW({ + globDirectory: 'dist/', + globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'], + swDest: 'dist/service-worker.js', + }) + .then(() => console.log('workbox done')) + + Promise.all([esbuildPromise, workboxPromise]).then(() => + console.log('build complete') + ) } diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index b3d0d40..e731f4e 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -21,8 +21,16 @@ "save": "Gem" }, "loading": { + "album": "Loader album", "default": "Loader...", - "shares": null + "media": "Loader medier", + "page": "Loader side", + "paginate": { + "faces": "Loader flere personer", + "media": "Loader flere medier" + }, + "shares": "Loader delinger...", + "timeline": "Loader tidslinje" } }, "header": { @@ -36,11 +44,6 @@ } } }, - "loading": { - "paginate": { - "media": "Loader flere medier" - } - }, "login_page": { "field": { "password": "Adgangskode", @@ -59,6 +62,16 @@ }, "welcome": "Velkommen til Photoview" }, + "people_page": { + "face_group": { + "label_placeholder": "Navn", + "unlabeled": "Ikke navngivet" + }, + "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter" + }, + "routes": { + "page_not_found": "Side ikke fundet" + }, "settings": { "concurrent_workers": { "description": "Det maksimale antal medier som må skannes samtidig", @@ -122,6 +135,15 @@ "title": "Brugere" } }, + "share_page": { + "protected_share": { + "description": "Denne deling er låst med en adgangskode.", + "title": "Beskyttet deling" + }, + "share_not_found": "Deling blev ikke fundet", + "share_not_found_description": "Måske er delingen udløbet eller blevet slettet.", + "wrong_password": "Forkert adgangskode, prøv venligst igen." + }, "sidebar": { "album": { "title": "Album indstillinger" @@ -202,6 +224,7 @@ }, "title": { "loading_album": "Loader album", + "people": "Personer", "settings": "Indstillinger" } } diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index 4edcd11..8ce673a 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -21,8 +21,16 @@ "save": "Save" }, "loading": { + "album": "Loading album", "default": "Loading...", - "shares": "Loading shares..." + "media": "Loading media", + "page": "Loading page", + "paginate": { + "faces": "Loading more people", + "media": "Loading more media" + }, + "shares": "Loading shares...", + "timeline": "Loading timeline" } }, "header": { @@ -36,11 +44,6 @@ } } }, - "loading": { - "paginate": { - "media": "Loading more media" - } - }, "login_page": { "field": { "password": "Password", @@ -59,6 +62,16 @@ }, "welcome": "Welcome to Photoview" }, + "people_page": { + "face_group": { + "label_placeholder": "Label", + "unlabeled": "Unlabeled" + }, + "recognize_unlabeled_faces_button": "Recognize unlabeled faces" + }, + "routes": { + "page_not_found": "Page not found" + }, "settings": { "concurrent_workers": { "description": "The maximum amount of scanner jobs that is allowed to run at once", @@ -89,7 +102,7 @@ "submit": "Add user" }, "confirm_delete_user": { - "action": "Delete {user}", + "action": "Delete {{user}}", "description": "<0>Are you sure, you want to delete <1>?

This action cannot be undone

", "title": "Delete user" }, @@ -122,6 +135,15 @@ "title": "Users" } }, + "share_page": { + "protected_share": { + "description": "This share is protected with a password.", + "title": "Protected share" + }, + "share_not_found": "Share not found", + "share_not_found_description": "Maybe the share has expired or has been deleted.", + "wrong_password": "Wrong password, please try again." + }, "sidebar": { "album": { "title": "Album options" @@ -202,6 +224,7 @@ }, "title": { "loading_album": "Loading album", + "people": "People", "settings": "Settings" } } diff --git a/ui/package.json b/ui/package.json index 68d1e70..5fcf0fd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -53,8 +53,8 @@ "workbox-build": "^6.1.2" }, "scripts": { - "start": "node build.mjs watch", - "build": "NODE_ENV=production node build.mjs", + "start": "node --experimental-modules build.mjs watch", + "build": "NODE_ENV=production node --experimental-modules build.mjs", "test": "npm run lint && npm run jest", "lint": "eslint ./src --max-warnings 0 --cache", "jest": "jest", diff --git a/ui/src/Layout.test.js b/ui/src/Layout.test.js index a7db600..b40a3aa 100644 --- a/ui/src/Layout.test.js +++ b/ui/src/Layout.test.js @@ -9,6 +9,8 @@ import { MemoryRouter } from 'react-router-dom' import * as authentication from './helpers/authentication' +require('./localization').default() + jest.mock('./helpers/authentication.js') test('Layout component', async () => { diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.js index f9b5180..01527b2 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.js +++ b/ui/src/Pages/AlbumPage/AlbumPage.js @@ -161,7 +161,7 @@ function AlbumPage({ match }) { /> ) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.js b/ui/src/Pages/PeoplePage/PeoplePage.js index 7f82c63..60fac48 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.js +++ b/ui/src/Pages/PeoplePage/PeoplePage.js @@ -9,6 +9,7 @@ import { Button, Icon, Input } from 'semantic-ui-react' import FaceCircleImage from './FaceCircleImage' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../../components/PaginateLoader' +import { useTranslation } from 'react-i18next' export const MY_FACES_QUERY = gql` query myFaces($limit: Int, $offset: Int) { @@ -73,6 +74,7 @@ const FaceDetailsButton = styled.button` const FaceLabel = styled.span`` const FaceDetails = ({ group }) => { + const { t } = useTranslation() const [editLabel, setEditLabel] = useState(false) const [inputValue, setInputValue] = useState(group.label ?? '') const inputRef = createRef() @@ -124,7 +126,9 @@ const FaceDetails = ({ group }) => { onClick={() => setEditLabel(true)} > {group.imageFaceCount} - {group.label ?? 'Unlabeled'} + + {group.label ?? t('people_page.face_group.unlabeled', 'Unlabeled')} + ) @@ -135,7 +139,7 @@ const FaceDetails = ({ group }) => { loading={loading} ref={inputRef} size="mini" - placeholder="Label" + placeholder={t('people_page.face_group.label_placeholder', 'Label')} icon="arrow right" value={inputValue} onKeyUp={onKeyUp} @@ -199,6 +203,7 @@ const FaceGroupsWrapper = styled.div` ` const PeopleGallery = () => { + const { t } = useTranslation() const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, { variables: { limit: 50, @@ -230,7 +235,7 @@ const PeopleGallery = () => { } return ( - + {faces} ) diff --git a/ui/src/Pages/SharePage/AlbumSharePage.js b/ui/src/Pages/SharePage/AlbumSharePage.js index 5f9f255..80045b7 100644 --- a/ui/src/Pages/SharePage/AlbumSharePage.js +++ b/ui/src/Pages/SharePage/AlbumSharePage.js @@ -4,6 +4,7 @@ import Layout from '../../Layout' import AlbumGallery from '../../components/albumGallery/AlbumGallery' import styled from 'styled-components' import { gql, useQuery } from '@apollo/client' +import { useTranslation } from 'react-i18next' export const SHARE_ALBUM_QUERY = gql` query shareAlbumQuery( @@ -73,6 +74,7 @@ const AlbumSharePageWrapper = styled.div` ` const AlbumSharePage = ({ albumID, token, password }) => { + const { t } = useTranslation() const { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, { variables: { id: albumID, @@ -88,14 +90,18 @@ const AlbumSharePage = ({ albumID, token, password }) => { } if (loading) { - return 'Loading...' + return t('general.loading.default', 'Loading...') } const album = data.album return ( - + `/share/${token}/${albumId}`} diff --git a/ui/src/Pages/SharePage/SharePage.js b/ui/src/Pages/SharePage/SharePage.js index fdc83bf..1188e2e 100644 --- a/ui/src/Pages/SharePage/SharePage.js +++ b/ui/src/Pages/SharePage/SharePage.js @@ -11,6 +11,7 @@ import { } from '../../helpers/authentication' import AlbumSharePage from './AlbumSharePage' import MediaSharePage from './MediaSharePage' +import { useTranslation } from 'react-i18next' export const SHARE_TOKEN_QUERY = gql` query SharePageToken($token: String!, $password: String) { @@ -71,6 +72,8 @@ export const VALIDATE_TOKEN_PASSWORD_QUERY = gql` ` const AuthorizedTokenRoute = ({ match }) => { + const { t } = useTranslation() + const token = match.params.token const password = getSharePassword(token) @@ -122,7 +125,7 @@ const AuthorizedTokenRoute = ({ match }) => { return } - return

Share not found

+ return

{t('share_page.share_not_found', 'Share not found')}

} AuthorizedTokenRoute.propTypes = { @@ -138,6 +141,8 @@ const ProtectedTokenEnterPassword = ({ refetchWithPassword, loading = false, }) => { + const { t } = useTranslation() + const [passwordValue, setPasswordValue] = useState('') const [invalidPassword, setInvalidPassword] = useState(false) @@ -150,7 +155,9 @@ const ProtectedTokenEnterPassword = ({ if (invalidPassword && !loading) { errorMessage = ( - Wrong password, please try again. + + {t('share_page.wrong_password', 'Wrong password, please try again.')} + ) } @@ -158,18 +165,23 @@ const ProtectedTokenEnterPassword = ({ return (
- Protected share + {t('share_page.protected_share.title', 'Protected share')}
-

This share is protected with a password.

+

+ {t( + 'share_page.protected_share.description', + 'This share is protected with a password.' + )} +

- + event.key == 'Enter' && onSubmit()} onChange={e => setPasswordValue(e.target.value)} - placeholder="Password" + placeholder={t('login_page.field.password', 'Password')} type="password" icon={} /> @@ -186,6 +198,8 @@ ProtectedTokenEnterPassword.propTypes = { } const TokenRoute = ({ match }) => { + const { t } = useTranslation() + const token = match.params.token const { loading, error, data, refetch } = useQuery( @@ -203,8 +217,13 @@ const TokenRoute = ({ match }) => { if (error.message == 'GraphQL error: share not found') { return ( -

Share not found

-

Maybe the share has expired or has been deleted.

+

{t('share_page.share_not_found', 'Share not found')}

+

+ {t( + 'share_page.share_not_found_description', + 'Maybe the share has expired or has been deleted.' + )} +

) } @@ -225,7 +244,7 @@ const TokenRoute = ({ match }) => { ) } - if (loading) return 'Loading...' + if (loading) return t('general.loading.default', 'Loading...') return } @@ -234,16 +253,20 @@ TokenRoute.propTypes = { match: PropTypes.object.isRequired, } -const SharePage = ({ match }) => ( - - - {({ match }) => { - return - }} - - Route not found - -) +const SharePage = ({ match }) => { + const { t } = useTranslation() + + return ( + + + {({ match }) => { + return + }} + + {t('routes.page_not_found', 'Page not found')} + + ) +} SharePage.propTypes = { ...RouterProps, diff --git a/ui/src/Pages/SharePage/SharePage.test.js b/ui/src/Pages/SharePage/SharePage.test.js index c894c16..fac0019 100644 --- a/ui/src/Pages/SharePage/SharePage.test.js +++ b/ui/src/Pages/SharePage/SharePage.test.js @@ -18,6 +18,8 @@ import SharePage, { import { SIDEBAR_DOWNLOAD_QUERY } from '../../components/sidebar/SidebarDownload' import { SHARE_ALBUM_QUERY } from './AlbumSharePage' +require('../../localization').default() + describe('load correct share page, based on graphql query', () => { const token = 'TOKEN123' diff --git a/ui/src/components/photoGallery/PhotoGallery.js b/ui/src/components/photoGallery/PhotoGallery.js index a672100..f3a0c66 100644 --- a/ui/src/components/photoGallery/PhotoGallery.js +++ b/ui/src/components/photoGallery/PhotoGallery.js @@ -6,6 +6,7 @@ import PresentView from './presentView/PresentView' import PropTypes from 'prop-types' import { SidebarContext } from '../sidebar/Sidebar' import MediaSidebar from '../sidebar/MediaSidebar' +import { useTranslation } from 'react-i18next' const Gallery = styled.div` display: flex; @@ -41,6 +42,7 @@ const PhotoGallery = ({ previousImage, onFavorite, }) => { + const { t } = useTranslation() const { updateSidebar } = useContext(SidebarContext) const activeImage = media && activeIndex != -1 && media[activeIndex] @@ -80,7 +82,9 @@ const PhotoGallery = ({ return ( - Loading images + + {t('general.loading.media', 'Loading media')} + {getPhotoElements(updateSidebar)} diff --git a/ui/src/components/routes/Routes.js b/ui/src/components/routes/Routes.js index 58593cb..f6b990b 100644 --- a/ui/src/components/routes/Routes.js +++ b/ui/src/components/routes/Routes.js @@ -4,6 +4,7 @@ import { Route, Switch, Redirect } from 'react-router-dom' import { Loader } from 'semantic-ui-react' import Layout from '../../Layout' import { clearTokenCookie } from '../../helpers/authentication' +import { useTranslation } from 'react-i18next' const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute')) @@ -26,11 +27,13 @@ const SettingsPage = React.lazy(() => ) const Routes = () => { + const { t } = useTranslation() + return ( - Loading page + {t('general.loading.page', 'Loading page')}
} > @@ -51,7 +54,11 @@ const Routes = () => { } /> -
Page not found
} /> + ( +
{t('routes.page_not_found', 'Page not found')}
+ )} + /> ) diff --git a/ui/src/components/routes/Routes.test.js b/ui/src/components/routes/Routes.test.js index 97dc92a..d3dfc1a 100644 --- a/ui/src/components/routes/Routes.test.js +++ b/ui/src/components/routes/Routes.test.js @@ -10,6 +10,8 @@ import { } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' +require('../../localization').default() + describe('routes', () => { test('unauthorized root path should navigate to login page', async () => { jest.mock('../../Pages/LoginPage/LoginPage.js', () => () => ( diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index 2954363..e1d2bdf 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -169,6 +169,7 @@ export const MetadataInfo = ({ media }) => { exif.focalLength = `${exif.focalLength}mm` } + const flash = flashLookup(t) if (!isNil(exif.flash) && flash[exif.flash]) { exif.flash = flash[exif.flash] } @@ -249,7 +250,7 @@ const exposureProgramsLookup = t => ({ }) // From https://exiftool.org/TagNames/EXIF.html#Flash -const flash = t => { +const flashLookup = t => { const values = { no_flash: t('sidebar.media.exif.flash.no_flash', 'No Flash'), fired: t('sidebar.media.exif.flash.fired', 'Fired'), diff --git a/ui/src/components/sidebar/MediaSidebar.test.js b/ui/src/components/sidebar/MediaSidebar.test.js index e94faf5..67ff5cb 100644 --- a/ui/src/components/sidebar/MediaSidebar.test.js +++ b/ui/src/components/sidebar/MediaSidebar.test.js @@ -4,6 +4,8 @@ import React from 'react' import { render, screen } from '@testing-library/react' import { MetadataInfo } from './MediaSidebar' +require('../../localization').default() + describe('MetadataInfo', () => { test('without EXIF information', async () => { const media = { @@ -33,11 +35,11 @@ describe('MetadataInfo', () => { expect(screen.queryByText('Maker')).not.toBeInTheDocument() expect(screen.queryByText('Lens')).not.toBeInTheDocument() expect(screen.queryByText('Program')).not.toBeInTheDocument() - expect(screen.queryByText('Date Shot')).not.toBeInTheDocument() + expect(screen.queryByText('Date shot')).not.toBeInTheDocument() expect(screen.queryByText('Exposure')).not.toBeInTheDocument() expect(screen.queryByText('Aperture')).not.toBeInTheDocument() expect(screen.queryByText('ISO')).not.toBeInTheDocument() - expect(screen.queryByText('Focal Length')).not.toBeInTheDocument() + expect(screen.queryByText('Focal length')).not.toBeInTheDocument() expect(screen.queryByText('Flash')).not.toBeInTheDocument() }) @@ -77,7 +79,7 @@ describe('MetadataInfo', () => { expect(screen.getByText('Program')).toBeInTheDocument() expect(screen.getByText('Canon EOS R')).toBeInTheDocument() - expect(screen.getByText('Date Shot')).toBeInTheDocument() + expect(screen.getByText('Date shot')).toBeInTheDocument() expect(screen.getByText('Exposure')).toBeInTheDocument() expect(screen.getByText('1/60')).toBeInTheDocument() @@ -91,7 +93,7 @@ describe('MetadataInfo', () => { expect(screen.getByText('ISO')).toBeInTheDocument() expect(screen.getByText('100')).toBeInTheDocument() - expect(screen.getByText('Focal Length')).toBeInTheDocument() + expect(screen.getByText('Focal length')).toBeInTheDocument() expect(screen.getByText('24mm')).toBeInTheDocument() expect(screen.getByText('Flash')).toBeInTheDocument() diff --git a/ui/src/components/timelineGallery/TimelineGallery.js b/ui/src/components/timelineGallery/TimelineGallery.js index 1dffd49..06a101e 100644 --- a/ui/src/components/timelineGallery/TimelineGallery.js +++ b/ui/src/components/timelineGallery/TimelineGallery.js @@ -10,6 +10,7 @@ import { FavoritesCheckbox } from '../AlbumFilter' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../PaginateLoader' import LazyLoad from '../../helpers/LazyLoad' +import { useTranslation } from 'react-i18next' const MY_TIMELINE_QUERY = gql` query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) { @@ -54,6 +55,7 @@ const GalleryWrapper = styled.div` ` const TimelineGallery = () => { + const { t } = useTranslation() const [activeIndex, setActiveIndex] = useState({ dateGroup: -1, albumGroup: -1, @@ -212,7 +214,9 @@ const TimelineGallery = () => { return ( <> - Loading timeline + + {t('general.loading.timeline', 'Loading timeline')} + { {timelineGroups} {presenting && ( { i18n.addResourceBundle('da', 'translation', danish) diff --git a/ui/src/localization.js b/ui/src/localization.js new file mode 100644 index 0000000..ca05f38 --- /dev/null +++ b/ui/src/localization.js @@ -0,0 +1,21 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' + +export default function setupLocalization() { + i18n.use(initReactI18next).init({ + resources: { + en: { + translation: { + 'Welcome to React': 'Welcome to React and react-i18next', + }, + }, + }, + lng: 'en', + fallbackLng: 'en', + returnNull: false, + + interpolation: { + escapeValue: false, + }, + }) +} From a881e0c9df09e81401998b09e2cd868676abbcb9 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 5 Apr 2021 23:18:38 +0200 Subject: [PATCH 13/28] Update node version in build process --- .github/workflows/tests.yml | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d199d3..47de507 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,7 +56,7 @@ jobs: strategy: matrix: - node-version: [10.x, 14.x] + node-version: [15.x] steps: - uses: actions/checkout@v2 diff --git a/Dockerfile b/Dockerfile index 265c51e..df74462 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ### Build UI ### -FROM --platform=${BUILDPLATFORM:-linux/amd64} node:10 as ui +FROM --platform=${BUILDPLATFORM:-linux/amd64} node:15 as ui ARG PHOTOVIEW_API_ENDPOINT ENV PHOTOVIEW_API_ENDPOINT=${PHOTOVIEW_API_ENDPOINT} From b6a85d0966752fefe8ccc7f2998d037aab200e31 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 11 Apr 2021 22:31:42 +0200 Subject: [PATCH 14/28] Finish localization - Add user preferences section to settings - Make settings page available for all users - Move log out button to settings page - Make it possible for each user to choose their desired language --- api/database/database.go | 1 + api/go.sum | 225 +++++ api/gqlgen.yml | 2 + api/graphql/directive.go | 25 +- api/graphql/generated.go | 859 +++++++++++++++++-- api/graphql/models/generated.go | 41 + api/graphql/models/user.go | 7 + api/graphql/resolvers/user.go | 43 + api/graphql/schema.graphql | 55 +- api/server.go | 3 +- ui/extractedTranslations/da/translation.json | 8 +- ui/extractedTranslations/en/translation.json | 8 +- ui/package.json | 6 +- ui/src/App.js | 39 +- ui/src/Layout.js | 16 +- ui/src/Pages/SettingsPage/SettingsPage.js | 21 +- ui/src/Pages/SettingsPage/UserPreferences.js | 78 ++ ui/src/Pages/SharePage/SharePage.js | 1 - ui/src/components/routes/AuthorizedRoute.js | 12 +- ui/src/components/routes/Routes.js | 2 +- ui/src/index.js | 7 - ui/src/localization.js | 4 + 22 files changed, 1337 insertions(+), 126 deletions(-) create mode 100644 ui/src/Pages/SettingsPage/UserPreferences.js diff --git a/api/database/database.go b/api/database/database.go index 84f5b54..74b97f3 100644 --- a/api/database/database.go +++ b/api/database/database.go @@ -163,6 +163,7 @@ func MigrateDatabase(db *gorm.DB) error { &models.VideoMetadata{}, &models.ShareToken{}, &models.UserMediaData{}, + &models.UserPreferences{}, // Face detection &models.FaceGroup{}, diff --git a/api/go.sum b/api/go.sum index e301fa0..02e0d65 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,115 +1,185 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/99designs/gqlgen v0.13.0 h1:haLTcUp3Vwp80xMVEg5KRNwzfUrgFdRmtBY8fuB8scA= github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Kagami/go-face v0.0.0-20200825065730-3dd2d74dccfb h1:DXwA1Te9paM+nsdTGc7uve37lq7WEbQO+gwGBPVwQuQ= github.com/Kagami/go-face v0.0.0-20200825065730-3dd2d74dccfb/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs= github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM= github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0 h1:qZ+woO4SamnH/eEbjM2IDLhRNwIwND/RQyVlBLp3Jqg= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/barasher/go-exiftool v1.4.0 h1:fTQsz1lWS15rRl13aFRjENt0gUXC+ZVBo/vICacn98A= github.com/barasher/go-exiftool v1.4.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= @@ -121,36 +191,60 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4= github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/hashicorp/consul/api v1.3.0 h1:HXNYlRkkM/t+Y/Yhxtwcy02dlYwIaoxzvxPnS+cqy78= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0 h1:UOxjlb4xVNF93jak1mzzoBatyFju9nrkxpVwIp/QqxQ= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0 h1:0U6+BtN6LhaYuTnIJq4Wyq5cpn6O2kWrxAtcqBmYY6w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= @@ -168,6 +262,7 @@ github.com/jackc/pgconn v1.8.1 h1:ySBX7Q87vOMqKU2bbmKbUvtYhauDFclYbNDYIE1/h6s= github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -206,43 +301,63 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0 h1:ZqfnKyx9KGpRcW04j5nnPDgRgoXUeLh2YFBeFzphcA0= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg= github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -250,17 +365,26 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -268,132 +392,203 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f h1:8P2MkG70G76gnZBOPGwmMIgwBb/rESQuwsJ7K8ds4NE= github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 h1:JJV9CsgM9EC9w2iVkwuz+sMx8yRFe89PJRUrv6hPCIA= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a h1:AhmOdSHeswKHBjhsLs/7+1voOxT+LLrSk/Nxvk35fug= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/strukturag/libheif v1.11.0 h1:HaWu5re98INSXNq7C8o5AwLcv2qD8+U7a+jVCpGWemI= github.com/strukturag/libheif v1.11.0/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns= github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xor-gate/goexif2 v1.1.0 h1:OvTZ5iEvsDhRWFjV5xY3wT7uHFna28nSSP7ucau+cXQ= github.com/xor-gate/goexif2 v1.1.0/go.mod h1:eRjn3VSkAwpNpxEx/CGmd0zg0JFGL3akrSMxnJ581AY= +github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -408,6 +603,7 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= @@ -416,8 +612,10 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -435,14 +633,17 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -464,7 +665,9 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -472,6 +675,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -490,6 +694,7 @@ golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM= golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -498,14 +703,17 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1 h1:oJra/lMfmtm13/rgY/8i3MzjFWYXvQIAKjQ3HqofMk8= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -515,25 +723,38 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/vansante/go-ffprobe.v2 v2.0.2 h1:DdxSfFnlqeawPIVbIQEI6LR6OQHQNR7tNgWb2mWuC4w= gopkg.in/vansante/go-ffprobe.v2 v2.0.2/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o= gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI= @@ -549,8 +770,12 @@ gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67 h1:e1sMhtVq9AfcEy8AXNb8eSg6gbzfdpYhoNqnPJa+GzI= sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/api/gqlgen.yml b/api/gqlgen.yml index 449ee44..2837525 100644 --- a/api/gqlgen.yml +++ b/api/gqlgen.yml @@ -27,6 +27,8 @@ models: fields: albums: resolver: true + UserPreferences: + model: github.com/photoview/photoview/api/graphql/models.UserPreferences Media: model: github.com/photoview/photoview/api/graphql/models.Media fields: diff --git a/api/graphql/directive.go b/api/graphql/directive.go index eec5427..a8126cc 100644 --- a/api/graphql/directive.go +++ b/api/graphql/directive.go @@ -6,17 +6,22 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/photoview/photoview/api/graphql/auth" - "gorm.io/gorm" ) -func IsAdmin(database *gorm.DB) func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { - return func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { - - user := auth.UserFromContext(ctx) - if user == nil || user.Admin == false { - return nil, errors.New("user must be admin") - } - - return next(ctx) +func IsAdmin(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { + user := auth.UserFromContext(ctx) + if user == nil || user.Admin == false { + return nil, errors.New("user must be admin") } + + return next(ctx) +} + +func IsAuthorized(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + return next(ctx) } diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 73d5467..c90bc6b 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -50,7 +50,8 @@ type ResolverRoot interface { } type DirectiveRoot struct { - IsAdmin func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) + IsAdmin func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) + IsAuthorized func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) } type ComplexityRoot struct { @@ -140,6 +141,7 @@ type ComplexityRoot struct { Mutation struct { AuthorizeUser func(childComplexity int, username string, password string) int + ChangeUserPreferences func(childComplexity int, language *string) int CombineFaceGroups func(childComplexity int, destinationFaceGroupID int, sourceFaceGroupID int) int CreateUser func(childComplexity int, username string, password *string, admin bool) int DeleteShareToken func(childComplexity int, token string) int @@ -185,6 +187,7 @@ type ComplexityRoot struct { MyMediaGeoJSON func(childComplexity int) int MyTimeline func(childComplexity int, paginate *models.Pagination, onlyFavorites *bool) int MyUser func(childComplexity int) int + MyUserPreferences func(childComplexity int) int Search func(childComplexity int, query string, limitMedia *int, limitAlbums *int) int ShareToken func(childComplexity int, credentials models.ShareTokenCredentials) int ShareTokenValidatePassword func(childComplexity int, credentials models.ShareTokenCredentials) int @@ -240,6 +243,11 @@ type ComplexityRoot struct { Username func(childComplexity int) int } + UserPreferences struct { + ID func(childComplexity int) int + Language func(childComplexity int) int + } + VideoMetadata struct { Audio func(childComplexity int) int Bitrate func(childComplexity int) int @@ -301,6 +309,7 @@ type MutationResolver interface { UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error) SetPeriodicScanInterval(ctx context.Context, interval int) (int, error) SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error) + ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error) SetFaceGroupLabel(ctx context.Context, faceGroupID int, label *string) (*models.FaceGroup, error) CombineFaceGroups(ctx context.Context, destinationFaceGroupID int, sourceFaceGroupID int) (*models.FaceGroup, error) MoveImageFaces(ctx context.Context, imageFaceIDs []int, destinationFaceGroupID int) (*models.FaceGroup, error) @@ -311,6 +320,7 @@ type QueryResolver interface { SiteInfo(ctx context.Context) (*models.SiteInfo, error) User(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.User, error) MyUser(ctx context.Context) (*models.User, error) + MyUserPreferences(ctx context.Context) (*models.UserPreferences, error) MyAlbums(ctx context.Context, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) @@ -777,6 +787,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AuthorizeUser(childComplexity, args["username"].(string), args["password"].(string)), true + case "Mutation.changeUserPreferences": + if e.complexity.Mutation.ChangeUserPreferences == nil { + break + } + + args, err := ec.field_Mutation_changeUserPreferences_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ChangeUserPreferences(childComplexity, args["language"].(*string)), true + case "Mutation.combineFaceGroups": if e.complexity.Mutation.CombineFaceGroups == nil { break @@ -1180,6 +1202,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.MyUser(childComplexity), true + case "Query.myUserPreferences": + if e.complexity.Query.MyUserPreferences == nil { + break + } + + return e.complexity.Query.MyUserPreferences(childComplexity), true + case "Query.search": if e.complexity.Query.Search == nil { break @@ -1424,6 +1453,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.Username(childComplexity), true + case "UserPreferences.id": + if e.complexity.UserPreferences.ID == nil { + break + } + + return e.complexity.UserPreferences.ID(childComplexity), true + + case "UserPreferences.language": + if e.complexity.UserPreferences.Language == nil { + break + } + + return e.complexity.UserPreferences.Language(childComplexity), true + case "VideoMetadata.audio": if e.complexity.VideoMetadata.Audio == nil { break @@ -1575,7 +1618,8 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var sources = []*ast.Source{ - {Name: "graphql/schema.graphql", Input: `directive @isAdmin on FIELD_DEFINITION + {Name: "graphql/schema.graphql", Input: `directive @isAuthorized on FIELD_DEFINITION +directive @isAdmin on FIELD_DEFINITION scalar Time scalar Any @@ -1607,7 +1651,9 @@ type Query { "List of registered users, must be admin to call" user(order: Ordering, paginate: Pagination): [User!]! @isAdmin "Information about the currently logged in user" - myUser: User! + myUser: User! @isAuthorized + + myUserPreferences: UserPreferences! @isAuthorized "List of albums owned by the logged in user." myAlbums( @@ -1619,7 +1665,7 @@ type Query { showEmpty: Boolean "Show only albums having favorites" onlyWithFavorites: Boolean - ): [Album!]! + ): [Album!]! @isAuthorized """ 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 @@ -1627,7 +1673,7 @@ type Query { album(id: ID!, tokenCredentials: ShareTokenCredentials): Album! "List of media owned by the logged in user" - myMedia(order: Ordering, paginate: Pagination): [Media!]! + myMedia(order: Ordering, paginate: Pagination): [Media!]! @isAuthorized """ Get media by id, user must own the media or be admin. If valid tokenCredentials are provided, the media may be retrived without further authentication @@ -1637,10 +1683,10 @@ type Query { "Get a list of media by their ids, user must own the media or be admin" mediaList(ids: [ID!]!): [Media!]! - myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]! + myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]! @isAuthorized "Get media owned by the logged in user, returned in GeoJson format" - myMediaGeoJson: Any! + myMediaGeoJson: Any! @isAuthorized "Get the mapbox api token, returns null if mapbox is not enabled" mapboxToken: String @@ -1649,8 +1695,8 @@ type Query { search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult! - myFaceGroups(paginate: Pagination): [FaceGroup!]! - faceGroup(id: ID!): FaceGroup! + myFaceGroups(paginate: Pagination): [FaceGroup!]! @isAuthorized + faceGroup(id: ID!): FaceGroup! @isAuthorized } type Mutation { @@ -1666,19 +1712,19 @@ type Mutation { "Scan all users for new media" scanAll: ScannerResult! @isAdmin "Scan a single user for new media" - scanUser(userId: ID!): ScannerResult! + scanUser(userId: ID!): ScannerResult! @isAdmin "Generate share token for album" - shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken + shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized "Generate share token for media" - shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken + shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized "Delete a share token by it's token value" - deleteShareToken(token: String!): ShareToken + deleteShareToken(token: String!): ShareToken @isAuthorized "Set a password for a token, if null is passed for the password argument, the password will be cleared" - protectShareToken(token: String!, password: String): ShareToken + protectShareToken(token: String!, password: String): ShareToken @isAuthorized "Mark or unmark a media as being a favorite" - favoriteMedia(mediaId: ID!, favorite: Boolean!): Media + favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized updateUser( id: ID! @@ -1701,21 +1747,23 @@ type Mutation { Set how often, in seconds, the server should automatically scan for new media, a value of 0 will disable periodic scans """ - setPeriodicScanInterval(interval: Int!): Int! + setPeriodicScanInterval(interval: Int!): Int! @isAdmin "Set max number of concurrent scanner jobs running at once" - setScannerConcurrentWorkers(workers: Int!): Int! + setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin + + changeUserPreferences(language: String): UserPreferences! @isAuthorized "Assign a label to a face group, set label to null to remove the current one" - setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! + setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized "Merge two face groups into a single one, all ImageFaces from source will be moved to destination" - combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup! + combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup! @isAuthorized "Move a list of ImageFaces to another face group" - moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! + moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! @isAuthorized "Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match" - recognizeUnlabeledFaces: [ImageFace!]! + recognizeUnlabeledFaces: [ImageFace!]! @isAuthorized "Move a list of ImageFaces to a new face group" - detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! + detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! @isAuthorized } type Subscription { @@ -1793,6 +1841,16 @@ type User { #shareTokens: [ShareToken] } +enum LanguageTranslation { + en, + da +} + +type UserPreferences { + id: ID! + language: LanguageTranslation +} + type Album { id: ID! title: String! @@ -2044,6 +2102,21 @@ func (ec *executionContext) field_Mutation_authorizeUser_args(ctx context.Contex return args, nil } +func (ec *executionContext) field_Mutation_changeUserPreferences_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 *string + if tmp, ok := rawArgs["language"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("language")) + arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["language"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_combineFaceGroups_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4921,8 +4994,28 @@ func (ec *executionContext) _Mutation_scanUser(ctx context.Context, field graphq } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().ScanUser(rctx, args["userId"].(int)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ScanUser(rctx, args["userId"].(int)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAdmin == nil { + return nil, errors.New("directive isAdmin is not implemented") + } + return ec.directives.IsAdmin(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.ScannerResult); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.ScannerResult`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -4963,8 +5056,28 @@ func (ec *executionContext) _Mutation_shareAlbum(ctx context.Context, field grap } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().ShareAlbum(rctx, args["albumId"].(int), args["expire"].(*time.Time), args["password"].(*string)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ShareAlbum(rctx, args["albumId"].(int), args["expire"].(*time.Time), args["password"].(*string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.ShareToken); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.ShareToken`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5002,8 +5115,28 @@ func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field grap } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().ShareMedia(rctx, args["mediaId"].(int), args["expire"].(*time.Time), args["password"].(*string)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ShareMedia(rctx, args["mediaId"].(int), args["expire"].(*time.Time), args["password"].(*string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.ShareToken); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.ShareToken`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5041,8 +5174,28 @@ func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, fiel } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DeleteShareToken(rctx, args["token"].(string)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteShareToken(rctx, args["token"].(string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.ShareToken); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.ShareToken`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5080,8 +5233,28 @@ func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, fie } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().ProtectShareToken(rctx, args["token"].(string), args["password"].(*string)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ProtectShareToken(rctx, args["token"].(string), args["password"].(*string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.ShareToken); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.ShareToken`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5119,8 +5292,28 @@ func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field g } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().FavoriteMedia(rctx, args["mediaId"].(int), args["favorite"].(bool)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().FavoriteMedia(rctx, args["mediaId"].(int), args["favorite"].(bool)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.Media); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.Media`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5453,8 +5646,28 @@ func (ec *executionContext) _Mutation_setPeriodicScanInterval(ctx context.Contex } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().SetPeriodicScanInterval(rctx, args["interval"].(int)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SetPeriodicScanInterval(rctx, args["interval"].(int)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAdmin == nil { + return nil, errors.New("directive isAdmin is not implemented") + } + return ec.directives.IsAdmin(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(int); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be int`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5495,8 +5708,28 @@ func (ec *executionContext) _Mutation_setScannerConcurrentWorkers(ctx context.Co } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().SetScannerConcurrentWorkers(rctx, args["workers"].(int)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SetScannerConcurrentWorkers(rctx, args["workers"].(int)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAdmin == nil { + return nil, errors.New("directive isAdmin is not implemented") + } + return ec.directives.IsAdmin(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(int); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be int`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5513,6 +5746,68 @@ func (ec *executionContext) _Mutation_setScannerConcurrentWorkers(ctx context.Co return ec.marshalNInt2int(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_changeUserPreferences(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_changeUserPreferences_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ChangeUserPreferences(rctx, args["language"].(*string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.UserPreferences); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.UserPreferences`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.UserPreferences) + fc.Result = res + return ec.marshalNUserPreferences2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_setFaceGroupLabel(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5537,8 +5832,28 @@ func (ec *executionContext) _Mutation_setFaceGroupLabel(ctx context.Context, fie } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().SetFaceGroupLabel(rctx, args["faceGroupID"].(int), args["label"].(*string)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SetFaceGroupLabel(rctx, args["faceGroupID"].(int), args["label"].(*string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.FaceGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.FaceGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5579,8 +5894,28 @@ func (ec *executionContext) _Mutation_combineFaceGroups(ctx context.Context, fie } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CombineFaceGroups(rctx, args["destinationFaceGroupID"].(int), args["sourceFaceGroupID"].(int)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CombineFaceGroups(rctx, args["destinationFaceGroupID"].(int), args["sourceFaceGroupID"].(int)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.FaceGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.FaceGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5621,8 +5956,28 @@ func (ec *executionContext) _Mutation_moveImageFaces(ctx context.Context, field } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().MoveImageFaces(rctx, args["imageFaceIDs"].([]int), args["destinationFaceGroupID"].(int)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().MoveImageFaces(rctx, args["imageFaceIDs"].([]int), args["destinationFaceGroupID"].(int)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.FaceGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.FaceGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5656,8 +6011,28 @@ func (ec *executionContext) _Mutation_recognizeUnlabeledFaces(ctx context.Contex ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().RecognizeUnlabeledFaces(rctx) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().RecognizeUnlabeledFaces(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*models.ImageFace); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/photoview/photoview/api/graphql/models.ImageFace`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -5698,8 +6073,28 @@ func (ec *executionContext) _Mutation_detachImageFaces(ctx context.Context, fiel } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().DetachImageFaces(rctx, args["imageFaceIDs"].([]int)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DetachImageFaces(rctx, args["imageFaceIDs"].([]int)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.FaceGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.FaceGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6104,8 +6499,28 @@ func (ec *executionContext) _Query_myUser(ctx context.Context, field graphql.Col ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MyUser(rctx) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MyUser(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.User); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.User`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6122,6 +6537,61 @@ func (ec *executionContext) _Query_myUser(ctx context.Context, field graphql.Col return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) } +func (ec *executionContext) _Query_myUserPreferences(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MyUserPreferences(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.UserPreferences); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.UserPreferences`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.UserPreferences) + fc.Result = res + return ec.marshalNUserPreferences2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_myAlbums(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -6146,8 +6616,28 @@ func (ec *executionContext) _Query_myAlbums(ctx context.Context, field graphql.C } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MyAlbums(rctx, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination), args["onlyRoot"].(*bool), args["showEmpty"].(*bool), args["onlyWithFavorites"].(*bool)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MyAlbums(rctx, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination), args["onlyRoot"].(*bool), args["showEmpty"].(*bool), args["onlyWithFavorites"].(*bool)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*models.Album); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/photoview/photoview/api/graphql/models.Album`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6230,8 +6720,28 @@ func (ec *executionContext) _Query_myMedia(ctx context.Context, field graphql.Co } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MyMedia(rctx, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MyMedia(rctx, args["order"].(*models.Ordering), args["paginate"].(*models.Pagination)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*models.Media); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/photoview/photoview/api/graphql/models.Media`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6356,8 +6866,28 @@ func (ec *executionContext) _Query_myTimeline(ctx context.Context, field graphql } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MyTimeline(rctx, args["paginate"].(*models.Pagination), args["onlyFavorites"].(*bool)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MyTimeline(rctx, args["paginate"].(*models.Pagination), args["onlyFavorites"].(*bool)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*models.TimelineGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/photoview/photoview/api/graphql/models.TimelineGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6391,8 +6921,28 @@ func (ec *executionContext) _Query_myMediaGeoJson(ctx context.Context, field gra ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MyMediaGeoJSON(rctx) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MyMediaGeoJSON(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(interface{}); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be interface{}`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6591,8 +7141,28 @@ func (ec *executionContext) _Query_myFaceGroups(ctx context.Context, field graph } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MyFaceGroups(rctx, args["paginate"].(*models.Pagination)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MyFaceGroups(rctx, args["paginate"].(*models.Pagination)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*models.FaceGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/photoview/photoview/api/graphql/models.FaceGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -6633,8 +7203,28 @@ func (ec *executionContext) _Query_faceGroup(ctx context.Context, field graphql. } fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().FaceGroup(rctx, args["id"].(int)) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().FaceGroup(rctx, args["id"].(int)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.FaceGroup); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.FaceGroup`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -7742,6 +8332,73 @@ func (ec *executionContext) _User_admin(ctx context.Context, field graphql.Colle return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _UserPreferences_id(ctx context.Context, field graphql.CollectedField, obj *models.UserPreferences) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "UserPreferences", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNID2int(ctx, field.Selections, res) +} + +func (ec *executionContext) _UserPreferences_language(ctx context.Context, field graphql.CollectedField, obj *models.UserPreferences) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "UserPreferences", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Language, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.LanguageTranslation) + fc.Result = res + return ec.marshalOLanguageTranslation2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐLanguageTranslation(ctx, field.Selections, res) +} + func (ec *executionContext) _VideoMetadata_id(ctx context.Context, field graphql.CollectedField, obj *models.VideoMetadata) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -9891,6 +10548,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "changeUserPreferences": + out.Values[i] = ec._Mutation_changeUserPreferences(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "setFaceGroupLabel": out.Values[i] = ec._Mutation_setFaceGroupLabel(ctx, field) if out.Values[i] == graphql.Null { @@ -10040,6 +10702,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "myUserPreferences": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_myUserPreferences(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) case "myAlbums": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -10528,6 +11204,35 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj return out } +var userPreferencesImplementors = []string{"UserPreferences"} + +func (ec *executionContext) _UserPreferences(ctx context.Context, sel ast.SelectionSet, obj *models.UserPreferences) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, userPreferencesImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("UserPreferences") + case "id": + out.Values[i] = ec._UserPreferences_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "language": + out.Values[i] = ec._UserPreferences_language(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var videoMetadataImplementors = []string{"VideoMetadata"} func (ec *executionContext) _VideoMetadata(ctx context.Context, sel ast.SelectionSet, obj *models.VideoMetadata) graphql.Marshaler { @@ -11487,6 +12192,20 @@ func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoview return ec._User(ctx, sel, v) } +func (ec *executionContext) marshalNUserPreferences2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx context.Context, sel ast.SelectionSet, v models.UserPreferences) graphql.Marshaler { + return ec._UserPreferences(ctx, sel, &v) +} + +func (ec *executionContext) marshalNUserPreferences2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx context.Context, sel ast.SelectionSet, v *models.UserPreferences) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._UserPreferences(ctx, sel, v) +} + func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } @@ -11803,6 +12522,22 @@ func (ec *executionContext) marshalOInt2ᚖint64(ctx context.Context, sel ast.Se return graphql.MarshalInt64(*v) } +func (ec *executionContext) unmarshalOLanguageTranslation2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐLanguageTranslation(ctx context.Context, v interface{}) (*models.LanguageTranslation, error) { + if v == nil { + return nil, nil + } + var res = new(models.LanguageTranslation) + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOLanguageTranslation2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐLanguageTranslation(ctx context.Context, sel ast.SelectionSet, v *models.LanguageTranslation) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return v +} + func (ec *executionContext) marshalOMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx context.Context, sel ast.SelectionSet, v *models.Media) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go index bfffd55..c2bfc5f 100644 --- a/api/graphql/models/generated.go +++ b/api/graphql/models/generated.go @@ -68,6 +68,47 @@ type TimelineGroup struct { Date time.Time `json:"date"` } +type LanguageTranslation string + +const ( + LanguageTranslationEn LanguageTranslation = "en" + LanguageTranslationDa LanguageTranslation = "da" +) + +var AllLanguageTranslation = []LanguageTranslation{ + LanguageTranslationEn, + LanguageTranslationDa, +} + +func (e LanguageTranslation) IsValid() bool { + switch e { + case LanguageTranslationEn, LanguageTranslationDa: + return true + } + return false +} + +func (e LanguageTranslation) String() string { + return string(e) +} + +func (e *LanguageTranslation) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = LanguageTranslation(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid LanguageTranslation", str) + } + return nil +} + +func (e LanguageTranslation) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + type MediaType string const ( diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go index 5b71e97..66dbb38 100644 --- a/api/graphql/models/user.go +++ b/api/graphql/models/user.go @@ -36,6 +36,13 @@ type AccessToken struct { Expire time.Time `gorm:"not null;index"` } +type UserPreferences struct { + Model + UserID int `gorm:"not null;index"` + User User `gorm:"constraint:OnDelete:CASCADE;"` + Language *LanguageTranslation +} + var ErrorInvalidUserCredentials = errors.New("invalid credentials") func AuthorizeUser(db *gorm.DB, username string, password string) (*User, error) { diff --git a/api/graphql/resolvers/user.go b/api/graphql/resolvers/user.go index a48bbb8..0646bdd 100644 --- a/api/graphql/resolvers/user.go +++ b/api/graphql/resolvers/user.go @@ -154,6 +154,49 @@ func (r *mutationResolver) InitialSetupWizard(ctx context.Context, username stri }, nil } +func (r *queryResolver) MyUserPreferences(ctx context.Context) (*models.UserPreferences, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + userPref := models.UserPreferences{ + UserID: user.ID, + } + if err := r.Database.Where("user_id = ?", user.ID).FirstOrCreate(&userPref).Error; err != nil { + return nil, err + } + + return &userPref, nil +} + +func (r *mutationResolver) ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + var langTrans *models.LanguageTranslation = nil + if language != nil { + lng := models.LanguageTranslation(*language) + langTrans = &lng + } + + var userPref models.UserPreferences + if err := r.Database.Where("user_id = ?", user.ID).FirstOrInit(&userPref).Error; err != nil { + return nil, err + } + + userPref.UserID = user.ID + userPref.Language = langTrans + + if err := r.Database.Save(&userPref).Error; err != nil { + return nil, err + } + + return &userPref, nil +} + // Admin queries func (r *mutationResolver) UpdateUser(ctx context.Context, id int, username *string, password *string, admin *bool) (*models.User, error) { diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 817fb0d..c6cd5b4 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -1,3 +1,4 @@ +directive @isAuthorized on FIELD_DEFINITION directive @isAdmin on FIELD_DEFINITION scalar Time @@ -30,7 +31,9 @@ type Query { "List of registered users, must be admin to call" user(order: Ordering, paginate: Pagination): [User!]! @isAdmin "Information about the currently logged in user" - myUser: User! + myUser: User! @isAuthorized + + myUserPreferences: UserPreferences! @isAuthorized "List of albums owned by the logged in user." myAlbums( @@ -42,7 +45,7 @@ type Query { showEmpty: Boolean "Show only albums having favorites" onlyWithFavorites: Boolean - ): [Album!]! + ): [Album!]! @isAuthorized """ 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 @@ -50,7 +53,7 @@ type Query { album(id: ID!, tokenCredentials: ShareTokenCredentials): Album! "List of media owned by the logged in user" - myMedia(order: Ordering, paginate: Pagination): [Media!]! + myMedia(order: Ordering, paginate: Pagination): [Media!]! @isAuthorized """ Get media by id, user must own the media or be admin. If valid tokenCredentials are provided, the media may be retrived without further authentication @@ -60,10 +63,10 @@ type Query { "Get a list of media by their ids, user must own the media or be admin" mediaList(ids: [ID!]!): [Media!]! - myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]! + myTimeline(paginate: Pagination, onlyFavorites: Boolean): [TimelineGroup!]! @isAuthorized "Get media owned by the logged in user, returned in GeoJson format" - myMediaGeoJson: Any! + myMediaGeoJson: Any! @isAuthorized "Get the mapbox api token, returns null if mapbox is not enabled" mapboxToken: String @@ -72,8 +75,8 @@ type Query { search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult! - myFaceGroups(paginate: Pagination): [FaceGroup!]! - faceGroup(id: ID!): FaceGroup! + myFaceGroups(paginate: Pagination): [FaceGroup!]! @isAuthorized + faceGroup(id: ID!): FaceGroup! @isAuthorized } type Mutation { @@ -89,19 +92,19 @@ type Mutation { "Scan all users for new media" scanAll: ScannerResult! @isAdmin "Scan a single user for new media" - scanUser(userId: ID!): ScannerResult! + scanUser(userId: ID!): ScannerResult! @isAdmin "Generate share token for album" - shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken + shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized "Generate share token for media" - shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken + shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized "Delete a share token by it's token value" - deleteShareToken(token: String!): ShareToken + deleteShareToken(token: String!): ShareToken @isAuthorized "Set a password for a token, if null is passed for the password argument, the password will be cleared" - protectShareToken(token: String!, password: String): ShareToken + protectShareToken(token: String!, password: String): ShareToken @isAuthorized "Mark or unmark a media as being a favorite" - favoriteMedia(mediaId: ID!, favorite: Boolean!): Media + favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized updateUser( id: ID! @@ -124,21 +127,23 @@ type Mutation { Set how often, in seconds, the server should automatically scan for new media, a value of 0 will disable periodic scans """ - setPeriodicScanInterval(interval: Int!): Int! + setPeriodicScanInterval(interval: Int!): Int! @isAdmin "Set max number of concurrent scanner jobs running at once" - setScannerConcurrentWorkers(workers: Int!): Int! + setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin + + changeUserPreferences(language: String): UserPreferences! @isAuthorized "Assign a label to a face group, set label to null to remove the current one" - setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! + setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized "Merge two face groups into a single one, all ImageFaces from source will be moved to destination" - combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup! + combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup! @isAuthorized "Move a list of ImageFaces to another face group" - moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! + moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! @isAuthorized "Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match" - recognizeUnlabeledFaces: [ImageFace!]! + recognizeUnlabeledFaces: [ImageFace!]! @isAuthorized "Move a list of ImageFaces to a new face group" - detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! + detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! @isAuthorized } type Subscription { @@ -216,6 +221,16 @@ type User { #shareTokens: [ShareToken] } +enum LanguageTranslation { + en, + da +} + +type UserPreferences { + id: ID! + language: LanguageTranslation +} + type Album { id: ID! title: String! diff --git a/api/server.go b/api/server.go index c423d33..5999a00 100644 --- a/api/server.go +++ b/api/server.go @@ -71,7 +71,8 @@ func main() { graphqlResolver := resolvers.Resolver{Database: db} graphqlDirective := photoview_graphql.DirectiveRoot{} - graphqlDirective.IsAdmin = photoview_graphql.IsAdmin(db) + graphqlDirective.IsAdmin = photoview_graphql.IsAdmin + graphqlDirective.IsAuthorized = photoview_graphql.IsAuthorized graphqlConfig := photoview_graphql.Config{ Resolvers: &graphqlResolver, diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index e731f4e..2eec906 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -77,6 +77,7 @@ "description": "Det maksimale antal medier som må skannes samtidig", "title": "Samtidige scanner-arbejdere" }, + "logout": null, "periodic_scanner": { "checkbox_label": "Aktiver periodiske scanner", "field": { @@ -97,6 +98,12 @@ "scan_all_users": "Scan alle brugere", "title": "Scanner" }, + "user_preferences": { + "language_selector": { + "placeholder": "Vælg sprog" + }, + "title": "Brugerindstillinger" + }, "users": { "add_user": { "submit": "Tilføj bruger" @@ -216,7 +223,6 @@ }, "sidemenu": { "albums": "Albums", - "logout": "Log af", "people": "Personer", "photos": "Billeder", "places": "Kort", diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index 8ce673a..c48252c 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -77,6 +77,7 @@ "description": "The maximum amount of scanner jobs that is allowed to run at once", "title": "Scanner concurrent workers" }, + "logout": "Log out", "periodic_scanner": { "checkbox_label": "Enable periodic scanner", "field": { @@ -97,6 +98,12 @@ "scan_all_users": "Scan all users", "title": "Scanner" }, + "user_preferences": { + "language_selector": { + "placeholder": "Select language" + }, + "title": "User preferences" + }, "users": { "add_user": { "submit": "Add user" @@ -216,7 +223,6 @@ }, "sidemenu": { "albums": "Albums", - "logout": "Log out", "people": "People", "photos": "Photos", "places": "Places", diff --git a/ui/package.json b/ui/package.json index 5fcf0fd..43c1d7d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -91,5 +91,9 @@ "lint-staged": { "*.{js,json,css,md,graphql}": "prettier --write", "*.js": "eslint --cache --fix --max-warnings 0" - } + }, + "sideEffects": [ + "./src/index.js", + "./src/localization.js" + ] } diff --git a/ui/src/App.js b/ui/src/App.js index 1da3187..ba3d0bf 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -1,8 +1,11 @@ -import React from 'react' +import React, { useEffect } from 'react' import { createGlobalStyle } from 'styled-components' import { Helmet } from 'react-helmet' import Routes from './components/routes/Routes' import Messages from './components/messages/Messages' +import i18n from 'i18next' +import { gql, useLazyQuery } from '@apollo/client' +import { authToken } from './helpers/authentication' const GlobalStyle = createGlobalStyle` * { @@ -28,7 +31,41 @@ const GlobalStyle = createGlobalStyle` import 'semantic-ui-css/semantic.min.css' +const SITE_TRANSLATION = gql` + query { + myUserPreferences { + id + language + } + } +` + +const loadTranslations = () => { + const [loadLang, { data }] = useLazyQuery(SITE_TRANSLATION) + + useEffect(() => { + if (authToken()) { + loadLang() + } + }, [authToken()]) + + useEffect(() => { + switch (data?.myUserPreferences.language) { + case 'da': + import('../extractedTranslations/da/translation.json').then(danish => { + i18n.addResourceBundle('da', 'translation', danish) + i18n.changeLanguage('da') + }) + break + default: + i18n.changeLanguage('en') + } + }, [data?.myUserPreferences.language]) +} + const App = () => { + loadTranslations() + return ( <> diff --git a/ui/src/Layout.js b/ui/src/Layout.js index 1a1799c..661ef09 100644 --- a/ui/src/Layout.js +++ b/ui/src/Layout.js @@ -98,10 +98,8 @@ const SideButtonLabel = styled.div` export const SideMenu = () => { const { t } = useTranslation() - const adminQuery = authToken() ? useQuery(ADMIN_QUERY) : null const mapboxQuery = authToken() ? useQuery(MAPBOX_QUERY) : null - const isAdmin = adminQuery?.data?.myUser?.admin const mapboxEnabled = !!mapboxQuery?.data?.mapboxToken return ( @@ -124,17 +122,9 @@ export const SideMenu = () => { {t('sidemenu.people', 'People')} - {isAdmin ? ( - - - - {t('sidemenu.settings', 'Settings')} - - - ) : null} - - - {t('sidemenu.logout', 'Log out')} + + + {t('sidemenu.settings', 'Settings')} ) diff --git a/ui/src/Pages/SettingsPage/SettingsPage.js b/ui/src/Pages/SettingsPage/SettingsPage.js index 4d832aa..31b80bf 100644 --- a/ui/src/Pages/SettingsPage/SettingsPage.js +++ b/ui/src/Pages/SettingsPage/SettingsPage.js @@ -1,10 +1,13 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { Button } from 'semantic-ui-react' import styled from 'styled-components' +import { useIsAdmin } from '../../components/routes/AuthorizedRoute' import Layout from '../../Layout' import ScannerSection from './ScannerSection' +import UserPreferences from './UserPreferences' import UsersTable from './Users/UsersTable' export const SectionTitle = styled.h2` @@ -26,11 +29,25 @@ export const InputLabelDescription = styled.p` const SettingsPage = () => { const { t } = useTranslation() + const isAdmin = useIsAdmin() return ( - - + + {isAdmin && ( + <> + + + + )} + ) } diff --git a/ui/src/Pages/SettingsPage/UserPreferences.js b/ui/src/Pages/SettingsPage/UserPreferences.js new file mode 100644 index 0000000..1a0cbab --- /dev/null +++ b/ui/src/Pages/SettingsPage/UserPreferences.js @@ -0,0 +1,78 @@ +import { useMutation, useQuery } from '@apollo/client' +import gql from 'graphql-tag' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Dropdown } from 'semantic-ui-react' +import styled from 'styled-components' + +import { SectionTitle } from './SettingsPage' + +const languagePreferences = [ + { key: 1, text: 'English', value: 'en' }, + { key: 2, text: 'Dansk', value: 'da' }, +] + +const CHANGE_USER_PREFERENCES = gql` + mutation($language: String) { + changeUserPreferences(language: $language) { + id + language + } + } +` + +const MY_USER_PREFERENCES = gql` + query { + myUserPreferences { + id + language + } + } +` + +const UserPreferencesWrapper = styled.div` + margin-bottom: 24px; +` + +const UserPreferences = () => { + const { t } = useTranslation() + + const { data } = useQuery(MY_USER_PREFERENCES) + + const [changePrefs, { loading: loadingPrefs, error }] = useMutation( + CHANGE_USER_PREFERENCES + ) + + if (error) { + return error.message + } + + return ( + + + {t('settings.user_preferences.title', 'User preferences')} + + { + changePrefs({ + variables: { + language, + }, + }) + }} + selection + value={data?.myUserPreferences.language} + loading={loadingPrefs} + disabled={loadingPrefs} + /> + + ) +} + +export default UserPreferences diff --git a/ui/src/Pages/SharePage/SharePage.js b/ui/src/Pages/SharePage/SharePage.js index 1188e2e..7620065 100644 --- a/ui/src/Pages/SharePage/SharePage.js +++ b/ui/src/Pages/SharePage/SharePage.js @@ -89,7 +89,6 @@ const AuthorizedTokenRoute = ({ match }) => { if (data.shareToken.album) { const SharedSubAlbumPage = ({ match }) => { - console.log('subalbum match', match) return ( { + const { data } = useQuery(ADMIN_QUERY) + return data?.myUser?.admin +} + export const Authorized = ({ children }) => { const token = authToken() @@ -20,6 +25,7 @@ export const Authorized = ({ children }) => { const AuthorizedRoute = ({ component: Component, admin = false, ...props }) => { const token = authToken() + const isAdmin = useIsAdmin() let unauthorizedRedirect = null if (!token) { @@ -28,11 +34,7 @@ const AuthorizedRoute = ({ component: Component, admin = false, ...props }) => { let adminRedirect = null if (token && admin) { - const { error, data } = useQuery(ADMIN_QUERY) - - if (error) alert(error) - - if (data && data.myUser && !data.myUser.admin) { + if (isAdmin === false) { adminRedirect = } } diff --git a/ui/src/components/routes/Routes.js b/ui/src/components/routes/Routes.js index f6b990b..0739752 100644 --- a/ui/src/components/routes/Routes.js +++ b/ui/src/components/routes/Routes.js @@ -52,7 +52,7 @@ const Routes = () => { - + } /> ( diff --git a/ui/src/index.js b/ui/src/index.js index fd16a88..a5e03df 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -7,17 +7,10 @@ import registerServiceWorker from './registerServiceWorker' import client from './apolloClient' import { ApolloProvider } from '@apollo/client' import { BrowserRouter as Router } from 'react-router-dom' -import i18n from 'i18next' import setupLocalization from './localization' setupLocalization() -import('../extractedTranslations/da/translation.json').then(danish => { - i18n.addResourceBundle('da', 'translation', danish) - console.log('loaded danish') - i18n.changeLanguage('da') -}) - const Main = () => ( diff --git a/ui/src/localization.js b/ui/src/localization.js index ca05f38..3890d3a 100644 --- a/ui/src/localization.js +++ b/ui/src/localization.js @@ -17,5 +17,9 @@ export default function setupLocalization() { interpolation: { escapeValue: false, }, + + react: { + useSuspense: process.env.NODE_ENV === 'production', + }, }) } From 7626c8292d863e0e62f23bc6d4bd79d31ea7f3d7 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 11 Apr 2021 22:43:21 +0200 Subject: [PATCH 15/28] Fix UI tests --- ui/src/components/routes/AuthorizedRoute.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/ui/src/components/routes/AuthorizedRoute.js b/ui/src/components/routes/AuthorizedRoute.js index 5c0a7e7..afc75a9 100644 --- a/ui/src/components/routes/AuthorizedRoute.js +++ b/ui/src/components/routes/AuthorizedRoute.js @@ -1,7 +1,7 @@ -import React from 'react' +import React, { useEffect } from 'react' import PropTypes from 'prop-types' import { Route, Redirect } from 'react-router-dom' -import { useQuery, gql } from '@apollo/client' +import { gql, useLazyQuery } from '@apollo/client' import { authToken } from '../../helpers/authentication' export const ADMIN_QUERY = gql` @@ -12,8 +12,19 @@ export const ADMIN_QUERY = gql` } ` -export const useIsAdmin = () => { - const { data } = useQuery(ADMIN_QUERY) +export const useIsAdmin = (enabled = true) => { + const [fetchAdminQuery, { data }] = useLazyQuery(ADMIN_QUERY) + + useEffect(() => { + if (authToken() && !data && enabled) { + fetchAdminQuery() + } + }, [authToken(), enabled]) + + if (!authToken()) { + return false + } + return data?.myUser?.admin } @@ -25,7 +36,7 @@ export const Authorized = ({ children }) => { const AuthorizedRoute = ({ component: Component, admin = false, ...props }) => { const token = authToken() - const isAdmin = useIsAdmin() + const isAdmin = useIsAdmin(admin) let unauthorizedRedirect = null if (!token) { From 084d9bfef7c114928b079c3f4a0c16ffb4156f9b Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 11 Apr 2021 22:55:44 +0200 Subject: [PATCH 16/28] Cleanup logs and external exif parser --- api/scanner/exif/exif_parser_external.go | 26 +++--------------------- api/scanner/scanner_user.go | 5 ++++- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/api/scanner/exif/exif_parser_external.go b/api/scanner/exif/exif_parser_external.go index 6a47e32..b151f1c 100644 --- a/api/scanner/exif/exif_parser_external.go +++ b/api/scanner/exif/exif_parser_external.go @@ -2,8 +2,6 @@ package exif import ( "log" - "regexp" - "strconv" "time" "github.com/barasher/go-exiftool" @@ -26,35 +24,31 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. for _, fileInfo := range fileInfos { if fileInfo.Err != nil { - log.Printf("Fileinfo error\n") + log.Printf("Fileinfo error: %v\n", fileInfo.Err) continue } // Get camera model model, err := fileInfo.GetString("Model") if err == nil { - log.Printf("Camera model: %v", model) newExif.Camera = &model } // Get Camera make make, err := fileInfo.GetString("Make") if err == nil { - log.Printf("Camera make: %v", make) newExif.Maker = &make } // Get lens lens, err := fileInfo.GetString("LensModel") if err == nil { - log.Printf("Lens: %v", lens) newExif.Lens = &lens } //Get time of photo date, err := fileInfo.GetString("DateTimeOriginal") if err == nil { - log.Printf("Date shot: %s", date) layout := "2006:01:02 15:04:05" dateTime, err := time.Parse(layout, date) if err == nil { @@ -65,68 +59,54 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. // Get exposure time exposureTime, err := fileInfo.GetFloat("ExposureTime") if err == nil { - log.Printf("Exposure time: %f", exposureTime) newExif.Exposure = &exposureTime } // Get aperture aperture, err := fileInfo.GetFloat("Aperture") if err == nil { - log.Printf("Aperture: %f", aperture) newExif.Aperture = &aperture } // Get ISO iso, err := fileInfo.GetInt("ISO") if err == nil { - log.Printf("ISO: %d", iso) newExif.Iso = &iso } // Get focal length - focalLen, err := fileInfo.GetString("FocalLength") + focalLen, err := fileInfo.GetFloat("FocalLength") if err == nil { - log.Printf("Focal length: %s", focalLen) - reg, _ := regexp.Compile("[0-9.]+") - focalLenStr := reg.FindString(focalLen) - focalLenFloat, err := strconv.ParseFloat(focalLenStr, 64) - if err == nil { - newExif.FocalLength = &focalLenFloat - } + newExif.FocalLength = &focalLen } // Get flash info flash, err := fileInfo.GetInt("Flash") if err == nil { - log.Printf("Flash: %d", flash) newExif.Flash = &flash } // Get orientation orientation, err := fileInfo.GetInt("Orientation") if err == nil { - log.Printf("Orientation: %d", orientation) newExif.Orientation = &orientation } // Get exposure program expProgram, err := fileInfo.GetInt("ExposureProgram") if err == nil { - log.Printf("Exposure Program: %d", expProgram) newExif.ExposureProgram = &expProgram } // GPS coordinates - longitude longitudeRaw, err := fileInfo.GetFloat("GPSLongitude") if err == nil { - log.Printf("GPS longitude: %f", longitudeRaw) newExif.GPSLongitude = &longitudeRaw } // GPS coordinates - latitude latitudeRaw, err := fileInfo.GetFloat("GPSLatitude") if err == nil { - log.Printf("GPS latitude: %f", latitudeRaw) newExif.GPSLatitude = &latitudeRaw } } diff --git a/api/scanner/scanner_user.go b/api/scanner/scanner_user.go index 96f68f6..431836a 100644 --- a/api/scanner/scanner_user.go +++ b/api/scanner/scanner_user.go @@ -13,7 +13,7 @@ import ( "github.com/photoview/photoview/api/graphql/notification" "github.com/photoview/photoview/api/utils" "github.com/pkg/errors" - "github.com/sabhiram/go-gitignore" + ignore "github.com/sabhiram/go-gitignore" "gorm.io/gorm" ) @@ -23,6 +23,9 @@ func getPhotoviewIgnore(ignorePath string) ([]string, error) { // Open .photoviewignore file, if exists photoviewIgnoreFile, err := os.Open(path.Join(ignorePath, ".photoviewignore")) if err != nil { + if err == os.ErrNotExist { + return photoviewIgnore, nil + } return photoviewIgnore, err } From 264ee54e7ff38eb0a54ef2b1d4820fdd4dde25ec Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 12 Apr 2021 00:14:27 +0200 Subject: [PATCH 17/28] Start on typescript transition --- api/graphql/generated.go | 8 +- api/graphql/models/generated.go | 14 +- api/graphql/schema.graphql | 8 +- ui/.eslintrc.js | 1 + ui/__generated__/globalTypes.ts | 33 ++ ui/apollo.config.js | 8 + ui/build.mjs | 2 +- ui/extractedTranslations/da/translation.json | 182 +++---- ui/extractedTranslations/en/translation.json | 2 +- ui/package-lock.json | 489 ++++++++++++------ ui/package.json | 31 +- ui/src/{App.js => App.tsx} | 15 +- ui/src/{Layout.js => Layout.tsx} | 37 +- .../AlbumPage/__generated__/albumQuery.ts | 118 +++++ .../__generated__/getMyAlbums.ts | 41 ++ .../LoginPage/__generated__/Authorize.ts | 24 + .../__generated__/CheckInitialSetup.ts | 17 + .../LoginPage/__generated__/InitialSetup.ts | 28 + ui/src/Pages/PeoplePage/PeoplePage.js | 2 +- .../SingleFaceGroup/MergeFaceGroupsModal.js | 2 +- .../__generated__/combineFaces.ts | 25 + .../__generated__/detachImageFaces.ts | 25 + .../__generated__/moveImageFaces.ts | 31 ++ .../__generated__/singleFaceGroup.ts | 82 +++ .../Pages/PeoplePage/__generated__/myFaces.ts | 65 +++ .../__generated__/recognizeUnlabeledFaces.ts | 20 + .../PeoplePage/__generated__/setGroupLabel.ts | 26 + .../__generated__/placePageMapboxToken.ts | 19 + .../__generated__/placePageQueryMedia.ts | 88 ++++ ui/src/Pages/SettingsPage/UserPreferences.js | 4 +- ui/src/Pages/SettingsPage/Users/AddUserRow.js | 4 +- .../Users/EditUserRowRootPaths.js | 9 +- .../Users/__generated__/changeUserPassword.ts | 22 + .../Users/__generated__/createUser.ts | 24 + .../Users/__generated__/deleteUser.ts | 22 + .../Users/__generated__/scanUser.ts | 24 + .../Users/__generated__/settingsUsersQuery.ts | 35 ++ .../Users/__generated__/updateUser.ts | 25 + .../Users/__generated__/userAddRootPath.ts | 25 + .../userRemoveAlbumPathMutation.ts | 22 + .../changeScanIntervalMutation.ts | 20 + .../__generated__/changeUserPreferences.ts | 24 + .../__generated__/concurrentWorkersQuery.ts | 20 + .../__generated__/myUserPreferences.ts | 20 + .../__generated__/scanAllMutation.ts | 21 + .../__generated__/scanIntervalQuery.ts | 20 + .../__generated__/setConcurrentWorkers.ts | 19 + .../SharePage/__generated__/SharePageToken.ts | 165 ++++++ .../ShareTokenValidatePassword.ts | 17 + .../__generated__/shareAlbumQuery.ts | 194 +++++++ ui/src/__generated__/adminQuery.ts | 20 + ui/src/__generated__/mapboxEnabledQuery.ts | 15 + ui/src/__generated__/siteTranslation.ts | 20 + .../__generated__/albumPathQuery.ts | 32 ++ .../header/__generated__/searchQuery.ts | 76 +++ .../__generated__/notificationSubscription.ts | 29 ++ .../__generated__/markMediaFavorite.ts | 26 + ui/src/components/routes/AuthorizedRoute.js | 11 +- .../routes/{Routes.js => Routes.tsx} | 0 .../sidebar/__generated__/getAlbumSidebar.ts | 26 + .../__generated__/sidbarGetAlbumShares.ts | 36 ++ .../__generated__/sidbarGetPhotoShares.ts | 36 ++ .../__generated__/sidebarAlbumAddShare.ts | 26 + .../__generated__/sidebarDownloadQuery.ts | 52 ++ .../sidebar/__generated__/sidebarPhoto.ts | 167 ++++++ .../__generated__/sidebarPhotoAddShare.ts | 26 + .../__generated__/sidebarProtectShare.ts | 29 ++ .../__generated__/sidebareDeleteShare.ts | 24 + .../__generated__/myTimeline.ts | 94 ++++ ui/src/{index.js => index.tsx} | 0 ui/tsconfig.json | 72 +++ 71 files changed, 2669 insertions(+), 327 deletions(-) create mode 100644 ui/__generated__/globalTypes.ts create mode 100644 ui/apollo.config.js rename ui/src/{App.js => App.tsx} (75%) rename ui/src/{Layout.js => Layout.tsx} (86%) create mode 100644 ui/src/Pages/AlbumPage/__generated__/albumQuery.ts create mode 100644 ui/src/Pages/AllAlbumsPage/__generated__/getMyAlbums.ts create mode 100644 ui/src/Pages/LoginPage/__generated__/Authorize.ts create mode 100644 ui/src/Pages/LoginPage/__generated__/CheckInitialSetup.ts create mode 100644 ui/src/Pages/LoginPage/__generated__/InitialSetup.ts create mode 100644 ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/combineFaces.ts create mode 100644 ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/detachImageFaces.ts create mode 100644 ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/moveImageFaces.ts create mode 100644 ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/singleFaceGroup.ts create mode 100644 ui/src/Pages/PeoplePage/__generated__/myFaces.ts create mode 100644 ui/src/Pages/PeoplePage/__generated__/recognizeUnlabeledFaces.ts create mode 100644 ui/src/Pages/PeoplePage/__generated__/setGroupLabel.ts create mode 100644 ui/src/Pages/PlacesPage/__generated__/placePageMapboxToken.ts create mode 100644 ui/src/Pages/PlacesPage/__generated__/placePageQueryMedia.ts create mode 100644 ui/src/Pages/SettingsPage/Users/__generated__/changeUserPassword.ts create mode 100644 ui/src/Pages/SettingsPage/Users/__generated__/createUser.ts create mode 100644 ui/src/Pages/SettingsPage/Users/__generated__/deleteUser.ts create mode 100644 ui/src/Pages/SettingsPage/Users/__generated__/scanUser.ts create mode 100644 ui/src/Pages/SettingsPage/Users/__generated__/settingsUsersQuery.ts create mode 100644 ui/src/Pages/SettingsPage/Users/__generated__/updateUser.ts create mode 100644 ui/src/Pages/SettingsPage/Users/__generated__/userAddRootPath.ts create mode 100644 ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts create mode 100644 ui/src/Pages/SettingsPage/__generated__/changeScanIntervalMutation.ts create mode 100644 ui/src/Pages/SettingsPage/__generated__/changeUserPreferences.ts create mode 100644 ui/src/Pages/SettingsPage/__generated__/concurrentWorkersQuery.ts create mode 100644 ui/src/Pages/SettingsPage/__generated__/myUserPreferences.ts create mode 100644 ui/src/Pages/SettingsPage/__generated__/scanAllMutation.ts create mode 100644 ui/src/Pages/SettingsPage/__generated__/scanIntervalQuery.ts create mode 100644 ui/src/Pages/SettingsPage/__generated__/setConcurrentWorkers.ts create mode 100644 ui/src/Pages/SharePage/__generated__/SharePageToken.ts create mode 100644 ui/src/Pages/SharePage/__generated__/ShareTokenValidatePassword.ts create mode 100644 ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts create mode 100644 ui/src/__generated__/adminQuery.ts create mode 100644 ui/src/__generated__/mapboxEnabledQuery.ts create mode 100644 ui/src/__generated__/siteTranslation.ts create mode 100644 ui/src/components/__generated__/albumPathQuery.ts create mode 100644 ui/src/components/header/__generated__/searchQuery.ts create mode 100644 ui/src/components/messages/__generated__/notificationSubscription.ts create mode 100644 ui/src/components/photoGallery/__generated__/markMediaFavorite.ts rename ui/src/components/routes/{Routes.js => Routes.tsx} (100%) create mode 100644 ui/src/components/sidebar/__generated__/getAlbumSidebar.ts create mode 100644 ui/src/components/sidebar/__generated__/sidbarGetAlbumShares.ts create mode 100644 ui/src/components/sidebar/__generated__/sidbarGetPhotoShares.ts create mode 100644 ui/src/components/sidebar/__generated__/sidebarAlbumAddShare.ts create mode 100644 ui/src/components/sidebar/__generated__/sidebarDownloadQuery.ts create mode 100644 ui/src/components/sidebar/__generated__/sidebarPhoto.ts create mode 100644 ui/src/components/sidebar/__generated__/sidebarPhotoAddShare.ts create mode 100644 ui/src/components/sidebar/__generated__/sidebarProtectShare.ts create mode 100644 ui/src/components/sidebar/__generated__/sidebareDeleteShare.ts create mode 100644 ui/src/components/timelineGallery/__generated__/myTimeline.ts rename ui/src/{index.js => index.tsx} (100%) create mode 100644 ui/tsconfig.json diff --git a/api/graphql/generated.go b/api/graphql/generated.go index c90bc6b..03d8520 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -1842,8 +1842,8 @@ type User { } enum LanguageTranslation { - en, - da + English, + Danish } type UserPreferences { @@ -1899,8 +1899,8 @@ type MediaDownload { } enum MediaType { - photo - video + Photo + Video } type Media { diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go index c2bfc5f..bea87e0 100644 --- a/api/graphql/models/generated.go +++ b/api/graphql/models/generated.go @@ -71,18 +71,18 @@ type TimelineGroup struct { type LanguageTranslation string const ( - LanguageTranslationEn LanguageTranslation = "en" - LanguageTranslationDa LanguageTranslation = "da" + LanguageTranslationEnglish LanguageTranslation = "English" + LanguageTranslationDanish LanguageTranslation = "Danish" ) var AllLanguageTranslation = []LanguageTranslation{ - LanguageTranslationEn, - LanguageTranslationDa, + LanguageTranslationEnglish, + LanguageTranslationDanish, } func (e LanguageTranslation) IsValid() bool { switch e { - case LanguageTranslationEn, LanguageTranslationDa: + case LanguageTranslationEnglish, LanguageTranslationDanish: return true } return false @@ -112,8 +112,8 @@ func (e LanguageTranslation) MarshalGQL(w io.Writer) { type MediaType string const ( - MediaTypePhoto MediaType = "photo" - MediaTypeVideo MediaType = "video" + MediaTypePhoto MediaType = "Photo" + MediaTypeVideo MediaType = "Video" ) var AllMediaType = []MediaType{ diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index c6cd5b4..bf81823 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -222,8 +222,8 @@ type User { } enum LanguageTranslation { - en, - da + English, + Danish } type UserPreferences { @@ -279,8 +279,8 @@ type MediaDownload { } enum MediaType { - photo - video + Photo + Video } type Media { diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 602967f..e5314a2 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -3,6 +3,7 @@ module.exports = { browser: true, es6: true, }, + ignorePatterns: ['**/*.ts', '**/*.tsx'], extends: ['eslint:recommended', 'plugin:react/recommended'], globals: { Atomics: 'readonly', diff --git a/ui/__generated__/globalTypes.ts b/ui/__generated__/globalTypes.ts new file mode 100644 index 0000000..2045400 --- /dev/null +++ b/ui/__generated__/globalTypes.ts @@ -0,0 +1,33 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +//============================================================== +// START Enums and Input Objects +//============================================================== + +export enum LanguageTranslation { + Danish = "Danish", + English = "English", +} + +export enum MediaType { + Photo = "Photo", + Video = "Video", +} + +export enum NotificationType { + Close = "Close", + Message = "Message", + Progress = "Progress", +} + +export enum OrderDirection { + ASC = "ASC", + DESC = "DESC", +} + +//============================================================== +// END Enums and Input Objects +//============================================================== diff --git a/ui/apollo.config.js b/ui/apollo.config.js new file mode 100644 index 0000000..6b72722 --- /dev/null +++ b/ui/apollo.config.js @@ -0,0 +1,8 @@ +module.exports = { + client: { + service: { + name: 'photoview', + localSchemaFile: '../api/graphql/schema.graphql', + }, + }, +} diff --git a/ui/build.mjs b/ui/build.mjs index f6605ad..3cab176 100644 --- a/ui/build.mjs +++ b/ui/build.mjs @@ -20,7 +20,7 @@ const defineEnv = ENVIRONMENT_VARIABLES.reduce((acc, key) => { }, {}) const esbuildOptions = { - entryPoints: ['src/index.js'], + entryPoints: ['src/index.tsx'], plugins: [ babel({ filter: /photoview\/ui\/src\/.*\.js$/, diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index 2eec906..2c3f540 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -1,12 +1,12 @@ { "album_filter": { - "only_favorites": "Vis kun favoritter", - "sort_by": "Sorter efter", + "only_favorites": null, + "sort_by": null, "sorting_options": { - "date_imported": "Dato for importering", - "date_shot": "Dato", - "title": "Titel", - "type": "Type" + "date_imported": null, + "date_shot": null, + "title": null, + "type": null } }, "albums_page": { @@ -14,33 +14,33 @@ }, "general": { "action": { - "add": "Tilføj", - "cancel": "Annuller", - "delete": "Slet", - "remove": "Fjern", - "save": "Gem" + "add": null, + "cancel": null, + "delete": null, + "remove": null, + "save": null }, "loading": { - "album": "Loader album", + "album": null, "default": "Loader...", - "media": "Loader medier", + "media": null, "page": "Loader side", "paginate": { "faces": "Loader flere personer", "media": "Loader flere medier" }, - "shares": "Loader delinger...", + "shares": null, "timeline": "Loader tidslinje" } }, "header": { "search": { - "loading": "Loader resultater...", - "no_results": "Fandt ingen resultater", - "placeholder": "Søg", + "loading": null, + "no_results": null, + "placeholder": null, "result_type": { - "albums": "Albums", - "photos": "Billeder" + "albums": null, + "photos": null } } }, @@ -74,24 +74,24 @@ }, "settings": { "concurrent_workers": { - "description": "Det maksimale antal medier som må skannes samtidig", - "title": "Samtidige scanner-arbejdere" + "description": null, + "title": null }, "logout": null, "periodic_scanner": { - "checkbox_label": "Aktiver periodiske scanner", + "checkbox_label": null, "field": { - "description": "Hvor ofte scanneren bør udføre automatiske scanninger af alle brugere", - "label": "Periodiske scanningsintervaller" + "description": null, + "label": null }, "interval_unit": { - "days": "Dage", - "hour": "Timer", - "minutes": "Minutter", - "months": "Måneder", - "seconds": "Sekunder" + "days": null, + "hour": null, + "minutes": null, + "months": null, + "seconds": null }, - "title": "Periodisk scanner" + "title": null }, "scanner": { "description": "Vil scanne alle brugere for nye eller opdaterede medier", @@ -106,21 +106,21 @@ }, "users": { "add_user": { - "submit": "Tilføj bruger" + "submit": null }, "confirm_delete_user": { - "action": "Slet {user}", - "description": "<0>Er du sikker på at du vil slette <1>?

Denne handling kan ikke fortrydes

", - "title": "Slet bruger" + "action": null, + "description": null, + "title": null }, "password_reset": { - "description": "Ændre adgangskode for <1>", + "description": null, "form": { - "label": "Ny adgangskode", - "placeholder": "adgangskode", - "submit": "Ændre adgangskode" + "label": null, + "placeholder": null, + "submit": null }, - "title": "Ændre adgangskode" + "title": null }, "table": { "column_names": { @@ -132,10 +132,10 @@ "new_user": "Ny bruger", "row": { "action": { - "change_password": "Ændre adgangskode", - "delete": "Slet", - "edit": "Rediger", - "scan": "Scan" + "change_password": null, + "delete": null, + "edit": null, + "scan": null } } }, @@ -153,72 +153,72 @@ }, "sidebar": { "album": { - "title": "Album indstillinger" + "title": null }, "download": { "filesize": { - "byte": "{{count}} Byte", - "byte_plural": "{{count}} Bytes", - "giga_byte": "{{count}} GB", - "kilo_byte": "{{count}} KB", - "mega_byte": "{{count}} MB", - "tera_byte": "{{count}} TB" + "byte": null, + "byte_plural": null, + "giga_byte": null, + "kilo_byte": null, + "mega_byte": null, + "tera_byte": null }, "table_columns": { - "dimensions": "Dimension", - "file_size": "Størrelse", - "file_type": "Type", - "name": "Navn" + "dimensions": null, + "file_size": null, + "file_type": null, + "name": null }, - "title": "Download" + "title": null }, "media": { "exif": { "exposure_program": { - "action_program": "Actionprogram", - "aperture_priority": "Blændeprioritet", - "bulb": "Bulb", - "creative_program": "Kreativ program", - "landscape_mode": "Landskabsmode", - "manual": "Manuel", - "normal_program": "Normal program", - "not_defined": "Ikke defineret", - "portrait_mode": "Portræt mode", - "shutter_priority": "Lukkerprioritet" + "action_program": null, + "aperture_priority": null, + "bulb": null, + "creative_program": null, + "landscape_mode": null, + "manual": null, + "normal_program": null, + "not_defined": null, + "portrait_mode": null, + "shutter_priority": null }, "flash": { - "auto": "Auto", - "did_not_fire": "Blitz affyrede ikke", - "fired": "Affyrede", - "no_flash": "Ingen blitz", - "no_flash_function": "Ingen blitz-funktion", - "off": "Slukket", - "on": "Tændt", - "red_eye_reduction": "Røde øjne reduktion", - "return_detected": "Retur registreret", - "return_not_detected": "Retur ikke registreret" + "auto": null, + "did_not_fire": null, + "fired": null, + "no_flash": null, + "no_flash_function": null, + "off": null, + "on": null, + "red_eye_reduction": null, + "return_detected": null, + "return_not_detected": null }, "name": { - "aperture": "Blænde", - "camera": "Kamera", - "date_shot": "Dato", - "exposure": "Lukketid", - "exposure_program": "Lukketid program", - "flash": "Blitz", - "focal_length": "Fokallængde", - "iso": "ISO", - "lens": "Lense", - "maker": "Mærke" + "aperture": null, + "camera": null, + "date_shot": null, + "exposure": null, + "exposure_program": null, + "flash": null, + "focal_length": null, + "iso": null, + "lens": null, + "maker": null } } }, "sharing": { - "add_share": "Tilføj deling", - "copy_link": "Kopier link", - "no_shares_found": "Ingen delinger fundet", - "public_link": "Offentligt link", - "table_header": "Offentlige delinger", - "title": "Indstillinger for deling" + "add_share": null, + "copy_link": null, + "no_shares_found": null, + "public_link": null, + "table_header": null, + "title": null } }, "sidemenu": { diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index c48252c..d5f801c 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -158,7 +158,7 @@ "download": { "filesize": { "byte": "{{count}} Byte", - "byte_plural": "{{count}} Bytes", + "byte_plural": null, "giga_byte": "{{count}} GB", "kilo_byte": "{{count}} KB", "mega_byte": "{{count}} MB", diff --git a/ui/package-lock.json b/ui/package-lock.json index 67884c0..ef8a704 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,11 +9,11 @@ "version": "0.0.1", "license": "GPL-3.0", "dependencies": { - "@apollo/client": "^3.3.13", - "@babel/core": "^7.13.14", + "@apollo/client": "^3.3.14", + "@babel/core": "^7.13.15", "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/plugin-transform-runtime": "^7.13.10", - "@babel/preset-env": "^7.13.12", + "@babel/plugin-transform-runtime": "^7.13.15", + "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", @@ -27,14 +27,13 @@ "dotenv": "^8.2.0", "esbuild": "^0.8.52", "esbuild-plugin-babel": "^0.2.3", - "eslint": "^7.23.0", - "eslint-plugin-jest": "^24.3.3", - "eslint-plugin-jest-dom": "^3.7.0", - "eslint-plugin-react": "^7.23.1", + "eslint": "^7.24.0", + "eslint-plugin-jest": "^24.3.5", + "eslint-plugin-jest-dom": "^3.8.0", + "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.2.0", "fs-extra": "^9.1.0", - "graphql": "^15.5.0", - "i18next": "^20.1.0", + "i18next": "^20.2.1", "mapbox-gl": "^2.2.0", "prop-types": "^15.7.2", "react": "^17.0.2", @@ -49,6 +48,7 @@ "semantic-ui-react": "^2.0.3", "styled-components": "^5.2.3", "subscriptions-transport-ws": "^0.9.18", + "typescript": "^4.2.4", "url-join": "^4.0.1", "workbox-build": "^6.1.2" }, @@ -56,6 +56,10 @@ "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", "@types/react": "^17.0.3", + "@types/react-dom": "^17.0.3", + "@types/react-helmet": "^6.1.1", + "@types/react-router-dom": "^5.1.7", + "@types/styled-components": "^5.1.9", "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^10.5.4", @@ -63,9 +67,9 @@ } }, "node_modules/@apollo/client": { - "version": "3.3.13", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.3.13.tgz", - "integrity": "sha512-usiVmXiOq0J/GpyIOIhlF8ItHpiPJObC7zoxLYPoOYj3G3OB0hCIcUKs3aTJ3ATW7u8QxvYgRaJg72NN7E1WOg==", + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.3.14.tgz", + "integrity": "sha512-z7YwMvF9grmpWUG+26e3gPcBAOA/r/Ci5gwK7JVm3bGYG9kKqG8MF6sMXEbuwTsFseE4duSp0icJ6tdzxJhhlA==", "dependencies": { "@graphql-typed-document-node/core": "^3.0.0", "@types/zen-observable": "^0.8.0", @@ -112,23 +116,23 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.12.tgz", - "integrity": "sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==" + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.15.tgz", + "integrity": "sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==" }, "node_modules/@babel/core": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.14.tgz", - "integrity": "sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.15.tgz", + "integrity": "sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ==", "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.13.9", "@babel/helper-compilation-targets": "^7.13.13", "@babel/helper-module-transforms": "^7.13.14", "@babel/helpers": "^7.13.10", - "@babel/parser": "^7.13.13", + "@babel/parser": "^7.13.15", "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", + "@babel/traverse": "^7.13.15", "@babel/types": "^7.13.14", "convert-source-map": "^1.7.0", "debug": "^4.1.0", @@ -252,9 +256,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.1.5.tgz", - "integrity": "sha512-nXuzCSwlJ/WKr8qxzW816gwyT6VZgiJG17zR40fou70yfAcqjoNyTLl/DQ+FExw5Hx5KNqshmN8Ldl/r2N7cTg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.0.tgz", + "integrity": "sha512-JT8tHuFjKBo8NnaUbblz7mIu1nnvUDiHVjXXkulZULyidvo/7P6TY7+YqpV37IfF+KUFxmlK04elKtGKXaiVgw==", "dependencies": { "@babel/helper-compilation-targets": "^7.13.0", "@babel/helper-module-imports": "^7.12.13", @@ -443,9 +447,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.13.tgz", - "integrity": "sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -467,9 +471,9 @@ } }, "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz", - "integrity": "sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.15.tgz", + "integrity": "sha512-VapibkWzFeoa6ubXy/NgV5U2U4MVnUlvnx6wo1XhlsaTrLYWE0UFpDQsVrmn22q5CzeloqJ8gEMHSKxuee6ZdA==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-remap-async-to-generator": "^7.13.0", @@ -1087,9 +1091,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.13.tgz", - "integrity": "sha512-lxb2ZAvSLyJ2PEe47hoGWPmW22v7CtSl9jW8mingV4H2sEX/JOcrAj2nPuGWi56ERUm2bUpjKzONAuT6HCn2EA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz", + "integrity": "sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ==", "dependencies": { "regenerator-transform": "^0.14.2" }, @@ -1109,15 +1113,15 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz", - "integrity": "sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz", + "integrity": "sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==", "dependencies": { - "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-module-imports": "^7.13.12", "@babel/helper-plugin-utils": "^7.13.0", - "babel-plugin-polyfill-corejs2": "^0.1.4", - "babel-plugin-polyfill-corejs3": "^0.1.3", - "babel-plugin-polyfill-regenerator": "^0.1.2", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", "semver": "^6.3.0" }, "peerDependencies": { @@ -1212,16 +1216,16 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.13.12.tgz", - "integrity": "sha512-JzElc6jk3Ko6zuZgBtjOd01pf9yYDEIH8BcqVuYIuOkzOwDesoa/Nz4gIo4lBG6K861KTV9TvIgmFuT6ytOaAA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.13.15.tgz", + "integrity": "sha512-D4JAPMXcxk69PKe81jRJ21/fP/uYdcTZ3hJDF5QX2HSI9bBxxYw/dumdR6dGumhjxlprHPE4XWoPaqzZUVy2MA==", "dependencies": { - "@babel/compat-data": "^7.13.12", - "@babel/helper-compilation-targets": "^7.13.10", + "@babel/compat-data": "^7.13.15", + "@babel/helper-compilation-targets": "^7.13.13", "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-validator-option": "^7.12.17", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.13.12", - "@babel/plugin-proposal-async-generator-functions": "^7.13.8", + "@babel/plugin-proposal-async-generator-functions": "^7.13.15", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-dynamic-import": "^7.13.8", "@babel/plugin-proposal-export-namespace-from": "^7.12.13", @@ -1269,7 +1273,7 @@ "@babel/plugin-transform-object-super": "^7.12.13", "@babel/plugin-transform-parameters": "^7.13.0", "@babel/plugin-transform-property-literals": "^7.12.13", - "@babel/plugin-transform-regenerator": "^7.12.13", + "@babel/plugin-transform-regenerator": "^7.13.15", "@babel/plugin-transform-reserved-words": "^7.12.13", "@babel/plugin-transform-shorthand-properties": "^7.12.13", "@babel/plugin-transform-spread": "^7.13.0", @@ -1279,10 +1283,10 @@ "@babel/plugin-transform-unicode-escapes": "^7.12.13", "@babel/plugin-transform-unicode-regex": "^7.12.13", "@babel/preset-modules": "^0.1.4", - "@babel/types": "^7.13.12", - "babel-plugin-polyfill-corejs2": "^0.1.4", - "babel-plugin-polyfill-corejs3": "^0.1.3", - "babel-plugin-polyfill-regenerator": "^0.1.2", + "@babel/types": "^7.13.14", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", "core-js-compat": "^3.9.0", "semver": "^6.3.0" }, @@ -1362,16 +1366,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.13.tgz", - "integrity": "sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.13.9", "@babel/helper-function-name": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.13.13", - "@babel/types": "^7.13.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", "debug": "^4.1.0", "globals": "^11.1.0" } @@ -2708,6 +2712,22 @@ "@types/node": "*" } }, + "node_modules/@types/history": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", + "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", + "dev": true + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dev": true, + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -2783,6 +2803,45 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz", + "integrity": "sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-helmet": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.1.tgz", + "integrity": "sha512-VmSCMz6jp/06DABoY60vQa++h1YFt0PfAI23llxBJHbowqFgLUL0dhS1AQeVPNqYfRp9LAfokrfWACTNeobOrg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.13.tgz", + "integrity": "sha512-ZIuaO9Yrln54X6elg8q2Ivp6iK6p4syPsefEYAhRDAoqNh48C8VYUmB9RkXjKSQAJSJV0mbIFCX7I4vZDcHrjg==", + "dev": true, + "dependencies": { + "@types/history": "*", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz", + "integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==", + "dev": true, + "dependencies": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -2803,6 +2862,17 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "node_modules/@types/styled-components": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.9.tgz", + "integrity": "sha512-kbEG6YlwK8rucITpKEr6pA4Ho9KSQHUUOzZ9lY3va1mtcjvS3D0wDciFyHEiNHKLL/npZCKDQJqm0x44sPO9oA==", + "dev": true, + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.9.5", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz", @@ -3477,12 +3547,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz", - "integrity": "sha512-DO95wD4g0A8KRaHKi0D51NdGXzvpqVLnLu5BTvDlpqUEpTmeEtypgC1xqesORaWmiUOQI14UHKlzNd9iZ2G3ZA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.0.tgz", + "integrity": "sha512-9bNwiR0dS881c5SHnzCmmGlMkJLl0OUZvxrxHo9w/iNoRuqaPjqlvBf4HrovXtQs/au5yKkpcdgfT1cC5PAZwg==", "dependencies": { - "@babel/compat-data": "^7.13.0", - "@babel/helper-define-polyfill-provider": "^0.1.5", + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.0", "semver": "^6.1.1" }, "peerDependencies": { @@ -3498,23 +3568,23 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.7.tgz", - "integrity": "sha512-u+gbS9bbPhZWEeyy1oR/YaaSpod/KDT07arZHb80aTpl8H5ZBq+uN1nN9/xtX7jQyfLdPfoqI4Rue/MQSWJquw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.0.tgz", + "integrity": "sha512-zZyi7p3BCUyzNxLx8KV61zTINkkV65zVkDAFNZmrTCRVhjo1jAS+YLvDJ9Jgd/w2tsAviCwFHReYfxO3Iql8Yg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.1.5", - "core-js-compat": "^3.8.1" + "@babel/helper-define-polyfill-provider": "^0.2.0", + "core-js-compat": "^3.9.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.1.6.tgz", - "integrity": "sha512-OUrYG9iKPKz8NxswXbRAdSwF0GhRdIEMTloQATJi4bDuFqrXaXcCUT/VGNrr8pBcjMh1RxZ7Xt9cytVJTJfvMg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.0.tgz", + "integrity": "sha512-J7vKbCuD2Xi/eEHxquHN14bXAW9CXtecwuLrOIDJtcZzTaPzV1VdEfoUf9AzcRBMolKUQKM9/GVojeh0hFiqMg==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.1.5" + "@babel/helper-define-polyfill-provider": "^0.2.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -4433,9 +4503,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.10.0.tgz", - "integrity": "sha512-9yVewub2MXNYyGvuLnMHcN1k9RkvB7/ofktpeKTIaASyB88YYqGzUnu0ywMMhJrDHOMiTjSHWGzR+i7Wb9Z1kQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.10.1.tgz", + "integrity": "sha512-ZHQTdTPkqvw2CeHiZC970NNJcnwzT6YIueDMASKt+p3WbZsLXOcoD392SkcWhkC0wBBHhlfhqGKKsNCQUozYtg==", "dependencies": { "browserslist": "^4.16.3", "semver": "7.0.0" @@ -5051,9 +5121,9 @@ } }, "node_modules/eslint": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.23.0.tgz", - "integrity": "sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.24.0.tgz", + "integrity": "sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==", "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.0", @@ -5104,9 +5174,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "24.3.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.3.tgz", - "integrity": "sha512-IQ9tLHyKEyBw1BM3IE13WxOXQm03h/7dy1KFknUVkoY2N2+Hw7lb/3YFz/4jwcrxXt2+KhA/GoiK7jt8aK19ww==", + "version": "24.3.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.5.tgz", + "integrity": "sha512-XG4rtxYDuJykuqhsOqokYIR84/C8pRihRtEpVskYLbIIKGwPNW2ySxdctuVzETZE+MbF/e7wmsnbNVpzM0rDug==", "dependencies": { "@typescript-eslint/experimental-utils": "^4.0.1" }, @@ -5124,9 +5194,9 @@ } }, "node_modules/eslint-plugin-jest-dom": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-3.7.0.tgz", - "integrity": "sha512-pLbKIV/upcORNROKgO6Yca13HGTkXvgcI7qaqJSZ8mvGMqaDvQSEh+RoabjeLByMzJBmAzNx1AAT2dUYOAVidw==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-3.8.0.tgz", + "integrity": "sha512-TsBK1RSG8mXCJaOce+gcNLU8ORl5yx111/HnsTTL70qwfb1AtbcqGtpz2OvfI/Q23baMtWDVy4L1KHgJk0B1XQ==", "dependencies": { "@babel/runtime": "^7.9.6", "@testing-library/dom": "^7.28.1", @@ -5142,9 +5212,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.1.tgz", - "integrity": "sha512-MvFGhZjI8Z4HusajmSw0ougGrq3Gs4vT/0WgwksZgf5RrLrRa2oYAw56okU4tZJl8+j7IYNuTM+2RnFEuTSdRQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz", + "integrity": "sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==", "dependencies": { "array-includes": "^3.1.3", "array.prototype.flatmap": "^1.2.4", @@ -6330,6 +6400,7 @@ "version": "15.5.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz", "integrity": "sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==", + "peer": true, "engines": { "node": ">= 10.x" } @@ -6605,9 +6676,9 @@ } }, "node_modules/i18next": { - "version": "20.1.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.1.0.tgz", - "integrity": "sha512-sV+ZwTM4Ik4d6wKdwNS/ocKmvXi6DFA/YHMgdQX3i4L5993jnbo1/j1pK/c4+zBOjexer4dt+c5JHsFj4CUoXQ==", + "version": "20.2.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.2.1.tgz", + "integrity": "sha512-JLruWDEQ3T6tKT6P7u+DsNtToMHUwUcQIYOMRcnNBdUhSfKkoIDUKdVDKgGtmqr//LrirxjADUdr3d5Gwbow6g==", "dependencies": { "@babel/runtime": "^7.12.0" } @@ -13169,6 +13240,18 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/ua-parser-js": { "version": "0.7.24", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.24.tgz", @@ -13866,9 +13949,9 @@ }, "dependencies": { "@apollo/client": { - "version": "3.3.13", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.3.13.tgz", - "integrity": "sha512-usiVmXiOq0J/GpyIOIhlF8ItHpiPJObC7zoxLYPoOYj3G3OB0hCIcUKs3aTJ3ATW7u8QxvYgRaJg72NN7E1WOg==", + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.3.14.tgz", + "integrity": "sha512-z7YwMvF9grmpWUG+26e3gPcBAOA/r/Ci5gwK7JVm3bGYG9kKqG8MF6sMXEbuwTsFseE4duSp0icJ6tdzxJhhlA==", "requires": { "@graphql-typed-document-node/core": "^3.0.0", "@types/zen-observable": "^0.8.0", @@ -13901,23 +13984,23 @@ } }, "@babel/compat-data": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.12.tgz", - "integrity": "sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==" + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.15.tgz", + "integrity": "sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==" }, "@babel/core": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.14.tgz", - "integrity": "sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.15.tgz", + "integrity": "sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ==", "requires": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.13.9", "@babel/helper-compilation-targets": "^7.13.13", "@babel/helper-module-transforms": "^7.13.14", "@babel/helpers": "^7.13.10", - "@babel/parser": "^7.13.13", + "@babel/parser": "^7.13.15", "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", + "@babel/traverse": "^7.13.15", "@babel/types": "^7.13.14", "convert-source-map": "^1.7.0", "debug": "^4.1.0", @@ -14017,9 +14100,9 @@ } }, "@babel/helper-define-polyfill-provider": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.1.5.tgz", - "integrity": "sha512-nXuzCSwlJ/WKr8qxzW816gwyT6VZgiJG17zR40fou70yfAcqjoNyTLl/DQ+FExw5Hx5KNqshmN8Ldl/r2N7cTg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.0.tgz", + "integrity": "sha512-JT8tHuFjKBo8NnaUbblz7mIu1nnvUDiHVjXXkulZULyidvo/7P6TY7+YqpV37IfF+KUFxmlK04elKtGKXaiVgw==", "requires": { "@babel/helper-compilation-targets": "^7.13.0", "@babel/helper-module-imports": "^7.12.13", @@ -14204,9 +14287,9 @@ } }, "@babel/parser": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.13.tgz", - "integrity": "sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==" + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==" }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.13.12", @@ -14219,9 +14302,9 @@ } }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz", - "integrity": "sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.15.tgz", + "integrity": "sha512-VapibkWzFeoa6ubXy/NgV5U2U4MVnUlvnx6wo1XhlsaTrLYWE0UFpDQsVrmn22q5CzeloqJ8gEMHSKxuee6ZdA==", "requires": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-remap-async-to-generator": "^7.13.0", @@ -14704,9 +14787,9 @@ } }, "@babel/plugin-transform-regenerator": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.13.tgz", - "integrity": "sha512-lxb2ZAvSLyJ2PEe47hoGWPmW22v7CtSl9jW8mingV4H2sEX/JOcrAj2nPuGWi56ERUm2bUpjKzONAuT6HCn2EA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz", + "integrity": "sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ==", "requires": { "regenerator-transform": "^0.14.2" } @@ -14720,15 +14803,15 @@ } }, "@babel/plugin-transform-runtime": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz", - "integrity": "sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz", + "integrity": "sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==", "requires": { - "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-module-imports": "^7.13.12", "@babel/helper-plugin-utils": "^7.13.0", - "babel-plugin-polyfill-corejs2": "^0.1.4", - "babel-plugin-polyfill-corejs3": "^0.1.3", - "babel-plugin-polyfill-regenerator": "^0.1.2", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", "semver": "^6.3.0" }, "dependencies": { @@ -14798,16 +14881,16 @@ } }, "@babel/preset-env": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.13.12.tgz", - "integrity": "sha512-JzElc6jk3Ko6zuZgBtjOd01pf9yYDEIH8BcqVuYIuOkzOwDesoa/Nz4gIo4lBG6K861KTV9TvIgmFuT6ytOaAA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.13.15.tgz", + "integrity": "sha512-D4JAPMXcxk69PKe81jRJ21/fP/uYdcTZ3hJDF5QX2HSI9bBxxYw/dumdR6dGumhjxlprHPE4XWoPaqzZUVy2MA==", "requires": { - "@babel/compat-data": "^7.13.12", - "@babel/helper-compilation-targets": "^7.13.10", + "@babel/compat-data": "^7.13.15", + "@babel/helper-compilation-targets": "^7.13.13", "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-validator-option": "^7.12.17", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.13.12", - "@babel/plugin-proposal-async-generator-functions": "^7.13.8", + "@babel/plugin-proposal-async-generator-functions": "^7.13.15", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-dynamic-import": "^7.13.8", "@babel/plugin-proposal-export-namespace-from": "^7.12.13", @@ -14855,7 +14938,7 @@ "@babel/plugin-transform-object-super": "^7.12.13", "@babel/plugin-transform-parameters": "^7.13.0", "@babel/plugin-transform-property-literals": "^7.12.13", - "@babel/plugin-transform-regenerator": "^7.12.13", + "@babel/plugin-transform-regenerator": "^7.13.15", "@babel/plugin-transform-reserved-words": "^7.12.13", "@babel/plugin-transform-shorthand-properties": "^7.12.13", "@babel/plugin-transform-spread": "^7.13.0", @@ -14865,10 +14948,10 @@ "@babel/plugin-transform-unicode-escapes": "^7.12.13", "@babel/plugin-transform-unicode-regex": "^7.12.13", "@babel/preset-modules": "^0.1.4", - "@babel/types": "^7.13.12", - "babel-plugin-polyfill-corejs2": "^0.1.4", - "babel-plugin-polyfill-corejs3": "^0.1.3", - "babel-plugin-polyfill-regenerator": "^0.1.2", + "@babel/types": "^7.13.14", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", "core-js-compat": "^3.9.0", "semver": "^6.3.0" }, @@ -14943,16 +15026,16 @@ } }, "@babel/traverse": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.13.tgz", - "integrity": "sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", "requires": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.13.9", "@babel/helper-function-name": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.13.13", - "@babel/types": "^7.13.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -16045,6 +16128,22 @@ "@types/node": "*" } }, + "@types/history": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", + "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", + "dev": true + }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dev": true, + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -16120,6 +16219,45 @@ "csstype": "^3.0.2" } }, + "@types/react-dom": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz", + "integrity": "sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-helmet": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.1.tgz", + "integrity": "sha512-VmSCMz6jp/06DABoY60vQa++h1YFt0PfAI23llxBJHbowqFgLUL0dhS1AQeVPNqYfRp9LAfokrfWACTNeobOrg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-router": { + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.13.tgz", + "integrity": "sha512-ZIuaO9Yrln54X6elg8q2Ivp6iK6p4syPsefEYAhRDAoqNh48C8VYUmB9RkXjKSQAJSJV0mbIFCX7I4vZDcHrjg==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz", + "integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -16140,6 +16278,17 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/styled-components": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.9.tgz", + "integrity": "sha512-kbEG6YlwK8rucITpKEr6pA4Ho9KSQHUUOzZ9lY3va1mtcjvS3D0wDciFyHEiNHKLL/npZCKDQJqm0x44sPO9oA==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, "@types/testing-library__jest-dom": { "version": "5.9.5", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz", @@ -16682,12 +16831,12 @@ } }, "babel-plugin-polyfill-corejs2": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz", - "integrity": "sha512-DO95wD4g0A8KRaHKi0D51NdGXzvpqVLnLu5BTvDlpqUEpTmeEtypgC1xqesORaWmiUOQI14UHKlzNd9iZ2G3ZA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.0.tgz", + "integrity": "sha512-9bNwiR0dS881c5SHnzCmmGlMkJLl0OUZvxrxHo9w/iNoRuqaPjqlvBf4HrovXtQs/au5yKkpcdgfT1cC5PAZwg==", "requires": { - "@babel/compat-data": "^7.13.0", - "@babel/helper-define-polyfill-provider": "^0.1.5", + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.0", "semver": "^6.1.1" }, "dependencies": { @@ -16699,20 +16848,20 @@ } }, "babel-plugin-polyfill-corejs3": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.7.tgz", - "integrity": "sha512-u+gbS9bbPhZWEeyy1oR/YaaSpod/KDT07arZHb80aTpl8H5ZBq+uN1nN9/xtX7jQyfLdPfoqI4Rue/MQSWJquw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.0.tgz", + "integrity": "sha512-zZyi7p3BCUyzNxLx8KV61zTINkkV65zVkDAFNZmrTCRVhjo1jAS+YLvDJ9Jgd/w2tsAviCwFHReYfxO3Iql8Yg==", "requires": { - "@babel/helper-define-polyfill-provider": "^0.1.5", - "core-js-compat": "^3.8.1" + "@babel/helper-define-polyfill-provider": "^0.2.0", + "core-js-compat": "^3.9.1" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.1.6.tgz", - "integrity": "sha512-OUrYG9iKPKz8NxswXbRAdSwF0GhRdIEMTloQATJi4bDuFqrXaXcCUT/VGNrr8pBcjMh1RxZ7Xt9cytVJTJfvMg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.0.tgz", + "integrity": "sha512-J7vKbCuD2Xi/eEHxquHN14bXAW9CXtecwuLrOIDJtcZzTaPzV1VdEfoUf9AzcRBMolKUQKM9/GVojeh0hFiqMg==", "requires": { - "@babel/helper-define-polyfill-provider": "^0.1.5" + "@babel/helper-define-polyfill-provider": "^0.2.0" } }, "babel-plugin-styled-components": { @@ -17466,9 +17615,9 @@ } }, "core-js-compat": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.10.0.tgz", - "integrity": "sha512-9yVewub2MXNYyGvuLnMHcN1k9RkvB7/ofktpeKTIaASyB88YYqGzUnu0ywMMhJrDHOMiTjSHWGzR+i7Wb9Z1kQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.10.1.tgz", + "integrity": "sha512-ZHQTdTPkqvw2CeHiZC970NNJcnwzT6YIueDMASKt+p3WbZsLXOcoD392SkcWhkC0wBBHhlfhqGKKsNCQUozYtg==", "requires": { "browserslist": "^4.16.3", "semver": "7.0.0" @@ -17965,9 +18114,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.23.0.tgz", - "integrity": "sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.24.0.tgz", + "integrity": "sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ==", "requires": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.0", @@ -18148,17 +18297,17 @@ } }, "eslint-plugin-jest": { - "version": "24.3.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.3.tgz", - "integrity": "sha512-IQ9tLHyKEyBw1BM3IE13WxOXQm03h/7dy1KFknUVkoY2N2+Hw7lb/3YFz/4jwcrxXt2+KhA/GoiK7jt8aK19ww==", + "version": "24.3.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.5.tgz", + "integrity": "sha512-XG4rtxYDuJykuqhsOqokYIR84/C8pRihRtEpVskYLbIIKGwPNW2ySxdctuVzETZE+MbF/e7wmsnbNVpzM0rDug==", "requires": { "@typescript-eslint/experimental-utils": "^4.0.1" } }, "eslint-plugin-jest-dom": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-3.7.0.tgz", - "integrity": "sha512-pLbKIV/upcORNROKgO6Yca13HGTkXvgcI7qaqJSZ8mvGMqaDvQSEh+RoabjeLByMzJBmAzNx1AAT2dUYOAVidw==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-3.8.0.tgz", + "integrity": "sha512-TsBK1RSG8mXCJaOce+gcNLU8ORl5yx111/HnsTTL70qwfb1AtbcqGtpz2OvfI/Q23baMtWDVy4L1KHgJk0B1XQ==", "requires": { "@babel/runtime": "^7.9.6", "@testing-library/dom": "^7.28.1", @@ -18166,9 +18315,9 @@ } }, "eslint-plugin-react": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.1.tgz", - "integrity": "sha512-MvFGhZjI8Z4HusajmSw0ougGrq3Gs4vT/0WgwksZgf5RrLrRa2oYAw56okU4tZJl8+j7IYNuTM+2RnFEuTSdRQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz", + "integrity": "sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==", "requires": { "array-includes": "^3.1.3", "array.prototype.flatmap": "^1.2.4", @@ -18939,7 +19088,8 @@ "graphql": { "version": "15.5.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz", - "integrity": "sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==" + "integrity": "sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==", + "peer": true }, "graphql-tag": { "version": "2.12.3", @@ -19159,9 +19309,9 @@ "dev": true }, "i18next": { - "version": "20.1.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.1.0.tgz", - "integrity": "sha512-sV+ZwTM4Ik4d6wKdwNS/ocKmvXi6DFA/YHMgdQX3i4L5993jnbo1/j1pK/c4+zBOjexer4dt+c5JHsFj4CUoXQ==", + "version": "20.2.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.2.1.tgz", + "integrity": "sha512-JLruWDEQ3T6tKT6P7u+DsNtToMHUwUcQIYOMRcnNBdUhSfKkoIDUKdVDKgGtmqr//LrirxjADUdr3d5Gwbow6g==", "requires": { "@babel/runtime": "^7.12.0" } @@ -24405,6 +24555,11 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==" + }, "ua-parser-js": { "version": "0.7.24", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.24.tgz", diff --git a/ui/package.json b/ui/package.json index 43c1d7d..24d6612 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,11 +9,11 @@ "license": "GPL-3.0", "description": "UI app for Photoview", "dependencies": { - "@apollo/client": "^3.3.13", - "@babel/core": "^7.13.14", + "@apollo/client": "^3.3.14", + "@babel/core": "^7.13.15", "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/plugin-transform-runtime": "^7.13.10", - "@babel/preset-env": "^7.13.12", + "@babel/plugin-transform-runtime": "^7.13.15", + "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", @@ -27,14 +27,13 @@ "dotenv": "^8.2.0", "esbuild": "^0.8.52", "esbuild-plugin-babel": "^0.2.3", - "eslint": "^7.23.0", - "eslint-plugin-jest": "^24.3.3", - "eslint-plugin-jest-dom": "^3.7.0", - "eslint-plugin-react": "^7.23.1", + "eslint": "^7.24.0", + "eslint-plugin-jest": "^24.3.5", + "eslint-plugin-jest-dom": "^3.8.0", + "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.2.0", "fs-extra": "^9.1.0", - "graphql": "^15.5.0", - "i18next": "^20.1.0", + "i18next": "^20.2.1", "mapbox-gl": "^2.2.0", "prop-types": "^15.7.2", "react": "^17.0.2", @@ -49,6 +48,7 @@ "semantic-ui-react": "^2.0.3", "styled-components": "^5.2.3", "subscriptions-transport-ws": "^0.9.18", + "typescript": "^4.2.4", "url-join": "^4.0.1", "workbox-build": "^6.1.2" }, @@ -58,12 +58,17 @@ "test": "npm run lint && npm run jest", "lint": "eslint ./src --max-warnings 0 --cache", "jest": "jest", + "genSchemaTypes": "npx apollo client:codegen --target=typescript", "prepare": "(cd .. && npx husky install)" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", "@types/react": "^17.0.3", + "@types/react-dom": "^17.0.3", + "@types/react-helmet": "^6.1.1", + "@types/react-router-dom": "^5.1.7", + "@types/styled-components": "^5.1.9", "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^10.5.4", @@ -91,9 +96,5 @@ "lint-staged": { "*.{js,json,css,md,graphql}": "prettier --write", "*.js": "eslint --cache --fix --max-warnings 0" - }, - "sideEffects": [ - "./src/index.js", - "./src/localization.js" - ] + } } diff --git a/ui/src/App.js b/ui/src/App.tsx similarity index 75% rename from ui/src/App.js rename to ui/src/App.tsx index ba3d0bf..dbbac51 100644 --- a/ui/src/App.js +++ b/ui/src/App.tsx @@ -30,9 +30,12 @@ const GlobalStyle = createGlobalStyle` ` import 'semantic-ui-css/semantic.min.css' +import { siteTranslation } from './__generated__/siteTranslation' +import { LanguageTranslation } from '../__generated__/globalTypes' +import { useTranslation } from 'react-i18next' const SITE_TRANSLATION = gql` - query { + query siteTranslation { myUserPreferences { id language @@ -41,7 +44,7 @@ const SITE_TRANSLATION = gql` ` const loadTranslations = () => { - const [loadLang, { data }] = useLazyQuery(SITE_TRANSLATION) + const [loadLang, { data }] = useLazyQuery(SITE_TRANSLATION) useEffect(() => { if (authToken()) { @@ -51,7 +54,7 @@ const loadTranslations = () => { useEffect(() => { switch (data?.myUserPreferences.language) { - case 'da': + case LanguageTranslation.Danish: import('../extractedTranslations/da/translation.json').then(danish => { i18n.addResourceBundle('da', 'translation', danish) i18n.changeLanguage('da') @@ -64,6 +67,7 @@ const loadTranslations = () => { } const App = () => { + const { t } = useTranslation() loadTranslations() return ( @@ -71,7 +75,10 @@ const App = () => { diff --git a/ui/src/Layout.js b/ui/src/Layout.tsx similarity index 86% rename from ui/src/Layout.js rename to ui/src/Layout.tsx index 661ef09..fe11940 100644 --- a/ui/src/Layout.js +++ b/ui/src/Layout.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { ReactChild, ReactChildren } from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' import { NavLink } from 'react-router-dom' @@ -79,16 +79,28 @@ const SideButtonLink = styled(NavLink)` } ` -const SideButton = props => { - return ( - - {props.children} - - ) +type SideButtonProps = { + children: any + to: string + exact: boolean } -SideButton.propTypes = { - children: PropTypes.any, +const SideButton = ({ + children, + to, + exact, + ...otherProps +}: SideButtonProps) => { + return ( + + {children} + + ) } const SideButtonLabel = styled.div` @@ -130,7 +142,12 @@ export const SideMenu = () => { ) } -const Layout = ({ children, title, ...otherProps }) => { +type LayoutProps = { + children: ReactChild | ReactChildren + title: string +} + +const Layout = ({ children, title, ...otherProps }: LayoutProps) => { return ( diff --git a/ui/src/Pages/AlbumPage/__generated__/albumQuery.ts b/ui/src/Pages/AlbumPage/__generated__/albumQuery.ts new file mode 100644 index 0000000..c61f267 --- /dev/null +++ b/ui/src/Pages/AlbumPage/__generated__/albumQuery.ts @@ -0,0 +1,118 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { OrderDirection, MediaType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: albumQuery +// ==================================================== + +export interface albumQuery_album_subAlbums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface albumQuery_album_subAlbums_thumbnail { + __typename: "Media"; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: albumQuery_album_subAlbums_thumbnail_thumbnail | null; +} + +export interface albumQuery_album_subAlbums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: albumQuery_album_subAlbums_thumbnail | null; +} + +export interface albumQuery_album_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 albumQuery_album_media_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface albumQuery_album_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface albumQuery_album_media { + __typename: "Media"; + id: string; + type: MediaType; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: albumQuery_album_media_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: albumQuery_album_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: albumQuery_album_media_videoWeb | null; + favorite: boolean; +} + +export interface albumQuery_album { + __typename: "Album"; + id: string; + title: string; + /** + * The albums contained in this album + */ + subAlbums: albumQuery_album_subAlbums[]; + /** + * The media inside this album + */ + media: albumQuery_album_media[]; +} + +export interface albumQuery { + /** + * 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 + */ + album: albumQuery_album; +} + +export interface albumQueryVariables { + id: string; + onlyFavorites?: boolean | null; + mediaOrderBy?: string | null; + mediaOrderDirection?: OrderDirection | null; + limit?: number | null; + offset?: number | null; +} diff --git a/ui/src/Pages/AllAlbumsPage/__generated__/getMyAlbums.ts b/ui/src/Pages/AllAlbumsPage/__generated__/getMyAlbums.ts new file mode 100644 index 0000000..33fb85e --- /dev/null +++ b/ui/src/Pages/AllAlbumsPage/__generated__/getMyAlbums.ts @@ -0,0 +1,41 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: getMyAlbums +// ==================================================== + +export interface getMyAlbums_myAlbums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface getMyAlbums_myAlbums_thumbnail { + __typename: "Media"; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: getMyAlbums_myAlbums_thumbnail_thumbnail | null; +} + +export interface getMyAlbums_myAlbums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: getMyAlbums_myAlbums_thumbnail | null; +} + +export interface getMyAlbums { + /** + * List of albums owned by the logged in user. + */ + myAlbums: getMyAlbums_myAlbums[]; +} diff --git a/ui/src/Pages/LoginPage/__generated__/Authorize.ts b/ui/src/Pages/LoginPage/__generated__/Authorize.ts new file mode 100644 index 0000000..2e59f1e --- /dev/null +++ b/ui/src/Pages/LoginPage/__generated__/Authorize.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: Authorize +// ==================================================== + +export interface Authorize_authorizeUser { + __typename: "AuthorizeResult"; + success: boolean; + status: string; + token: string | null; +} + +export interface Authorize { + authorizeUser: Authorize_authorizeUser; +} + +export interface AuthorizeVariables { + username: string; + password: string; +} diff --git a/ui/src/Pages/LoginPage/__generated__/CheckInitialSetup.ts b/ui/src/Pages/LoginPage/__generated__/CheckInitialSetup.ts new file mode 100644 index 0000000..04e813d --- /dev/null +++ b/ui/src/Pages/LoginPage/__generated__/CheckInitialSetup.ts @@ -0,0 +1,17 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: CheckInitialSetup +// ==================================================== + +export interface CheckInitialSetup_siteInfo { + __typename: "SiteInfo"; + initialSetup: boolean; +} + +export interface CheckInitialSetup { + siteInfo: CheckInitialSetup_siteInfo; +} diff --git a/ui/src/Pages/LoginPage/__generated__/InitialSetup.ts b/ui/src/Pages/LoginPage/__generated__/InitialSetup.ts new file mode 100644 index 0000000..ba42b9e --- /dev/null +++ b/ui/src/Pages/LoginPage/__generated__/InitialSetup.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: InitialSetup +// ==================================================== + +export interface InitialSetup_initialSetupWizard { + __typename: "AuthorizeResult"; + success: boolean; + status: string; + token: string | null; +} + +export interface InitialSetup { + /** + * Registers the initial user, can only be called if initialSetup from SiteInfo is true + */ + initialSetupWizard: InitialSetup_initialSetupWizard | null; +} + +export interface InitialSetupVariables { + username: string; + password: string; + rootPath: string; +} diff --git a/ui/src/Pages/PeoplePage/PeoplePage.js b/ui/src/Pages/PeoplePage/PeoplePage.js index 60fac48..991447b 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.js +++ b/ui/src/Pages/PeoplePage/PeoplePage.js @@ -39,7 +39,7 @@ export const MY_FACES_QUERY = gql` ` export const SET_GROUP_LABEL_MUTATION = gql` - mutation($groupID: ID!, $label: String) { + mutation setGroupLabel($groupID: ID!, $label: String) { setFaceGroupLabel(faceGroupID: $groupID, label: $label) { id label diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js index 9d7dff2..6d724fa 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal.js @@ -7,7 +7,7 @@ import { MY_FACES_QUERY } from '../PeoplePage' import SelectFaceGroupTable from './SelectFaceGroupTable' const COMBINE_FACES_MUTATION = gql` - mutation($destID: ID!, $srcID: ID!) { + mutation combineFaces($destID: ID!, $srcID: ID!) { combineFaceGroups( destinationFaceGroupID: $destID sourceFaceGroupID: $srcID diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/combineFaces.ts b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/combineFaces.ts new file mode 100644 index 0000000..23f6c44 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/combineFaces.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: combineFaces +// ==================================================== + +export interface combineFaces_combineFaceGroups { + __typename: "FaceGroup"; + id: string; +} + +export interface combineFaces { + /** + * Merge two face groups into a single one, all ImageFaces from source will be moved to destination + */ + combineFaceGroups: combineFaces_combineFaceGroups; +} + +export interface combineFacesVariables { + destID: string; + srcID: string; +} diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/detachImageFaces.ts b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/detachImageFaces.ts new file mode 100644 index 0000000..ad423de --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/detachImageFaces.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: detachImageFaces +// ==================================================== + +export interface detachImageFaces_detachImageFaces { + __typename: "FaceGroup"; + id: string; + label: string | null; +} + +export interface detachImageFaces { + /** + * Move a list of ImageFaces to a new face group + */ + detachImageFaces: detachImageFaces_detachImageFaces; +} + +export interface detachImageFacesVariables { + faceIDs: string[]; +} diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/moveImageFaces.ts b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/moveImageFaces.ts new file mode 100644 index 0000000..1279f42 --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/moveImageFaces.ts @@ -0,0 +1,31 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: moveImageFaces +// ==================================================== + +export interface moveImageFaces_moveImageFaces_imageFaces { + __typename: "ImageFace"; + id: string; +} + +export interface moveImageFaces_moveImageFaces { + __typename: "FaceGroup"; + id: string; + imageFaces: moveImageFaces_moveImageFaces_imageFaces[]; +} + +export interface moveImageFaces { + /** + * Move a list of ImageFaces to another face group + */ + moveImageFaces: moveImageFaces_moveImageFaces; +} + +export interface moveImageFacesVariables { + faceIDs: string[]; + destFaceGroupID: string; +} diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/singleFaceGroup.ts b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/singleFaceGroup.ts new file mode 100644 index 0000000..08ac01e --- /dev/null +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/__generated__/singleFaceGroup.ts @@ -0,0 +1,82 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: singleFaceGroup +// ==================================================== + +export interface singleFaceGroup_faceGroup_imageFaces_rectangle { + __typename: "FaceRectangle"; + minX: number; + maxX: number; + minY: number; + maxY: number; +} + +export interface singleFaceGroup_faceGroup_imageFaces_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 singleFaceGroup_faceGroup_imageFaces_media_highRes { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface singleFaceGroup_faceGroup_imageFaces_media { + __typename: "Media"; + id: string; + type: MediaType; + title: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: singleFaceGroup_faceGroup_imageFaces_media_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: singleFaceGroup_faceGroup_imageFaces_media_highRes | null; + favorite: boolean; +} + +export interface singleFaceGroup_faceGroup_imageFaces { + __typename: "ImageFace"; + id: string; + rectangle: singleFaceGroup_faceGroup_imageFaces_rectangle | null; + media: singleFaceGroup_faceGroup_imageFaces_media; +} + +export interface singleFaceGroup_faceGroup { + __typename: "FaceGroup"; + id: string; + label: string | null; + imageFaces: singleFaceGroup_faceGroup_imageFaces[]; +} + +export interface singleFaceGroup { + faceGroup: singleFaceGroup_faceGroup; +} + +export interface singleFaceGroupVariables { + id: string; + limit: number; + offset: number; +} diff --git a/ui/src/Pages/PeoplePage/__generated__/myFaces.ts b/ui/src/Pages/PeoplePage/__generated__/myFaces.ts new file mode 100644 index 0000000..22e17e4 --- /dev/null +++ b/ui/src/Pages/PeoplePage/__generated__/myFaces.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: myFaces +// ==================================================== + +export interface myFaces_myFaceGroups_imageFaces_rectangle { + __typename: "FaceRectangle"; + minX: number; + maxX: number; + minY: number; + maxY: number; +} + +export interface myFaces_myFaceGroups_imageFaces_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 myFaces_myFaceGroups_imageFaces_media { + __typename: "Media"; + id: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: myFaces_myFaceGroups_imageFaces_media_thumbnail | null; +} + +export interface myFaces_myFaceGroups_imageFaces { + __typename: "ImageFace"; + id: string; + rectangle: myFaces_myFaceGroups_imageFaces_rectangle | null; + media: myFaces_myFaceGroups_imageFaces_media; +} + +export interface myFaces_myFaceGroups { + __typename: "FaceGroup"; + id: string; + label: string | null; + imageFaceCount: number; + imageFaces: myFaces_myFaceGroups_imageFaces[]; +} + +export interface myFaces { + myFaceGroups: myFaces_myFaceGroups[]; +} + +export interface myFacesVariables { + limit?: number | null; + offset?: number | null; +} diff --git a/ui/src/Pages/PeoplePage/__generated__/recognizeUnlabeledFaces.ts b/ui/src/Pages/PeoplePage/__generated__/recognizeUnlabeledFaces.ts new file mode 100644 index 0000000..bfbfb49 --- /dev/null +++ b/ui/src/Pages/PeoplePage/__generated__/recognizeUnlabeledFaces.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: recognizeUnlabeledFaces +// ==================================================== + +export interface recognizeUnlabeledFaces_recognizeUnlabeledFaces { + __typename: "ImageFace"; + id: string; +} + +export interface recognizeUnlabeledFaces { + /** + * Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match + */ + recognizeUnlabeledFaces: recognizeUnlabeledFaces_recognizeUnlabeledFaces[]; +} diff --git a/ui/src/Pages/PeoplePage/__generated__/setGroupLabel.ts b/ui/src/Pages/PeoplePage/__generated__/setGroupLabel.ts new file mode 100644 index 0000000..4ca3620 --- /dev/null +++ b/ui/src/Pages/PeoplePage/__generated__/setGroupLabel.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: setGroupLabel +// ==================================================== + +export interface setGroupLabel_setFaceGroupLabel { + __typename: "FaceGroup"; + id: string; + label: string | null; +} + +export interface setGroupLabel { + /** + * Assign a label to a face group, set label to null to remove the current one + */ + setFaceGroupLabel: setGroupLabel_setFaceGroupLabel; +} + +export interface setGroupLabelVariables { + groupID: string; + label?: string | null; +} diff --git a/ui/src/Pages/PlacesPage/__generated__/placePageMapboxToken.ts b/ui/src/Pages/PlacesPage/__generated__/placePageMapboxToken.ts new file mode 100644 index 0000000..e519734 --- /dev/null +++ b/ui/src/Pages/PlacesPage/__generated__/placePageMapboxToken.ts @@ -0,0 +1,19 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: placePageMapboxToken +// ==================================================== + +export interface placePageMapboxToken { + /** + * Get the mapbox api token, returns null if mapbox is not enabled + */ + mapboxToken: string | null; + /** + * Get media owned by the logged in user, returned in GeoJson format + */ + myMediaGeoJson: any; +} diff --git a/ui/src/Pages/PlacesPage/__generated__/placePageQueryMedia.ts b/ui/src/Pages/PlacesPage/__generated__/placePageQueryMedia.ts new file mode 100644 index 0000000..311338d --- /dev/null +++ b/ui/src/Pages/PlacesPage/__generated__/placePageQueryMedia.ts @@ -0,0 +1,88 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: placePageQueryMedia +// ==================================================== + +export interface placePageQueryMedia_mediaList_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 placePageQueryMedia_mediaList_highRes { + __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 placePageQueryMedia_mediaList_videoWeb { + __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 placePageQueryMedia_mediaList { + __typename: "Media"; + id: string; + title: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: placePageQueryMedia_mediaList_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: placePageQueryMedia_mediaList_highRes | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: placePageQueryMedia_mediaList_videoWeb | null; + type: MediaType; +} + +export interface placePageQueryMedia { + /** + * Get a list of media by their ids, user must own the media or be admin + */ + mediaList: placePageQueryMedia_mediaList[]; +} + +export interface placePageQueryMediaVariables { + mediaIDs: string[]; +} diff --git a/ui/src/Pages/SettingsPage/UserPreferences.js b/ui/src/Pages/SettingsPage/UserPreferences.js index 1a0cbab..b67d6d0 100644 --- a/ui/src/Pages/SettingsPage/UserPreferences.js +++ b/ui/src/Pages/SettingsPage/UserPreferences.js @@ -13,7 +13,7 @@ const languagePreferences = [ ] const CHANGE_USER_PREFERENCES = gql` - mutation($language: String) { + mutation changeUserPreferences($language: String) { changeUserPreferences(language: $language) { id language @@ -22,7 +22,7 @@ const CHANGE_USER_PREFERENCES = gql` ` const MY_USER_PREFERENCES = gql` - query { + query myUserPreferences { myUserPreferences { id language diff --git a/ui/src/Pages/SettingsPage/Users/AddUserRow.js b/ui/src/Pages/SettingsPage/Users/AddUserRow.js index 3d87f44..831239f 100644 --- a/ui/src/Pages/SettingsPage/Users/AddUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/AddUserRow.js @@ -15,7 +15,7 @@ const createUserMutation = gql` } ` -const addRootPathMutation = gql` +export const userAddRootPathMutation = gql` mutation userAddRootPath($id: ID!, $rootPath: String!) { userAddRootPath(id: $id, rootPath: $rootPath) { id @@ -35,7 +35,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { const [state, setState] = useState(initialState) const [addRootPath, { loading: addRootPathLoading }] = useMutation( - addRootPathMutation, + userAddRootPathMutation, { onCompleted: () => { setState(initialState) diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js index 11e84bc..ddc5c00 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js @@ -5,14 +5,7 @@ import { Button, Icon, Input } from 'semantic-ui-react' import styled from 'styled-components' import { USERS_QUERY } from './UsersTable' import { useTranslation } from 'react-i18next' - -const userAddRootPathMutation = gql` - mutation userAddRootPath($id: ID!, $rootPath: String!) { - userAddRootPath(id: $id, rootPath: $rootPath) { - id - } - } -` +import { userAddRootPathMutation } from './AddUserRow' const userRemoveAlbumPathMutation = gql` mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) { diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/changeUserPassword.ts b/ui/src/Pages/SettingsPage/Users/__generated__/changeUserPassword.ts new file mode 100644 index 0000000..a2fc65c --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/changeUserPassword.ts @@ -0,0 +1,22 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: changeUserPassword +// ==================================================== + +export interface changeUserPassword_updateUser { + __typename: "User"; + id: string; +} + +export interface changeUserPassword { + updateUser: changeUserPassword_updateUser | null; +} + +export interface changeUserPasswordVariables { + userId: string; + password: string; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/createUser.ts b/ui/src/Pages/SettingsPage/Users/__generated__/createUser.ts new file mode 100644 index 0000000..87db9bc --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/createUser.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: createUser +// ==================================================== + +export interface createUser_createUser { + __typename: "User"; + id: string; + username: string; + admin: boolean; +} + +export interface createUser { + createUser: createUser_createUser | null; +} + +export interface createUserVariables { + username: string; + admin: boolean; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/deleteUser.ts b/ui/src/Pages/SettingsPage/Users/__generated__/deleteUser.ts new file mode 100644 index 0000000..fb130b3 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/deleteUser.ts @@ -0,0 +1,22 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: deleteUser +// ==================================================== + +export interface deleteUser_deleteUser { + __typename: "User"; + id: string; + username: string; +} + +export interface deleteUser { + deleteUser: deleteUser_deleteUser | null; +} + +export interface deleteUserVariables { + id: string; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/scanUser.ts b/ui/src/Pages/SettingsPage/Users/__generated__/scanUser.ts new file mode 100644 index 0000000..37f0c92 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/scanUser.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: scanUser +// ==================================================== + +export interface scanUser_scanUser { + __typename: "ScannerResult"; + success: boolean; +} + +export interface scanUser { + /** + * Scan a single user for new media + */ + scanUser: scanUser_scanUser; +} + +export interface scanUserVariables { + userId: string; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/settingsUsersQuery.ts b/ui/src/Pages/SettingsPage/Users/__generated__/settingsUsersQuery.ts new file mode 100644 index 0000000..9de9605 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/settingsUsersQuery.ts @@ -0,0 +1,35 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: settingsUsersQuery +// ==================================================== + +export interface settingsUsersQuery_user_rootAlbums { + __typename: "Album"; + id: string; + /** + * The path on the filesystem of the server, where this album is located + */ + filePath: string; +} + +export interface settingsUsersQuery_user { + __typename: "User"; + id: string; + username: string; + admin: boolean; + /** + * Top level albums owned by this user + */ + rootAlbums: settingsUsersQuery_user_rootAlbums[]; +} + +export interface settingsUsersQuery { + /** + * List of registered users, must be admin to call + */ + user: settingsUsersQuery_user[]; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/updateUser.ts b/ui/src/Pages/SettingsPage/Users/__generated__/updateUser.ts new file mode 100644 index 0000000..fcd282d --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/updateUser.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: updateUser +// ==================================================== + +export interface updateUser_updateUser { + __typename: "User"; + id: string; + username: string; + admin: boolean; +} + +export interface updateUser { + updateUser: updateUser_updateUser | null; +} + +export interface updateUserVariables { + id: string; + username?: string | null; + admin?: boolean | null; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/userAddRootPath.ts b/ui/src/Pages/SettingsPage/Users/__generated__/userAddRootPath.ts new file mode 100644 index 0000000..666df55 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/userAddRootPath.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: userAddRootPath +// ==================================================== + +export interface userAddRootPath_userAddRootPath { + __typename: "Album"; + id: string; +} + +export interface userAddRootPath { + /** + * Add a root path from where to look for media for the given user + */ + userAddRootPath: userAddRootPath_userAddRootPath | null; +} + +export interface userAddRootPathVariables { + id: string; + rootPath: string; +} diff --git a/ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts b/ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts new file mode 100644 index 0000000..9d16cb2 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/__generated__/userRemoveAlbumPathMutation.ts @@ -0,0 +1,22 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: userRemoveAlbumPathMutation +// ==================================================== + +export interface userRemoveAlbumPathMutation_userRemoveRootAlbum { + __typename: "Album"; + id: string; +} + +export interface userRemoveAlbumPathMutation { + userRemoveRootAlbum: userRemoveAlbumPathMutation_userRemoveRootAlbum | null; +} + +export interface userRemoveAlbumPathMutationVariables { + userId: string; + albumId: string; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/changeScanIntervalMutation.ts b/ui/src/Pages/SettingsPage/__generated__/changeScanIntervalMutation.ts new file mode 100644 index 0000000..f1ff320 --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/changeScanIntervalMutation.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: changeScanIntervalMutation +// ==================================================== + +export interface changeScanIntervalMutation { + /** + * Set how often, in seconds, the server should automatically scan for new media, + * a value of 0 will disable periodic scans + */ + setPeriodicScanInterval: number; +} + +export interface changeScanIntervalMutationVariables { + interval: number; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/changeUserPreferences.ts b/ui/src/Pages/SettingsPage/__generated__/changeUserPreferences.ts new file mode 100644 index 0000000..a198727 --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/changeUserPreferences.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { LanguageTranslation } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: changeUserPreferences +// ==================================================== + +export interface changeUserPreferences_changeUserPreferences { + __typename: "UserPreferences"; + id: string; + language: LanguageTranslation | null; +} + +export interface changeUserPreferences { + changeUserPreferences: changeUserPreferences_changeUserPreferences; +} + +export interface changeUserPreferencesVariables { + language?: string | null; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/concurrentWorkersQuery.ts b/ui/src/Pages/SettingsPage/__generated__/concurrentWorkersQuery.ts new file mode 100644 index 0000000..b1bfe3e --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/concurrentWorkersQuery.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: concurrentWorkersQuery +// ==================================================== + +export interface concurrentWorkersQuery_siteInfo { + __typename: "SiteInfo"; + /** + * How many max concurrent scanner jobs that should run at once + */ + concurrentWorkers: number; +} + +export interface concurrentWorkersQuery { + siteInfo: concurrentWorkersQuery_siteInfo; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/myUserPreferences.ts b/ui/src/Pages/SettingsPage/__generated__/myUserPreferences.ts new file mode 100644 index 0000000..4f8e690 --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/myUserPreferences.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { LanguageTranslation } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: myUserPreferences +// ==================================================== + +export interface myUserPreferences_myUserPreferences { + __typename: "UserPreferences"; + id: string; + language: LanguageTranslation | null; +} + +export interface myUserPreferences { + myUserPreferences: myUserPreferences_myUserPreferences; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/scanAllMutation.ts b/ui/src/Pages/SettingsPage/__generated__/scanAllMutation.ts new file mode 100644 index 0000000..95fa9b3 --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/scanAllMutation.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: scanAllMutation +// ==================================================== + +export interface scanAllMutation_scanAll { + __typename: "ScannerResult"; + success: boolean; + message: string | null; +} + +export interface scanAllMutation { + /** + * Scan all users for new media + */ + scanAll: scanAllMutation_scanAll; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/scanIntervalQuery.ts b/ui/src/Pages/SettingsPage/__generated__/scanIntervalQuery.ts new file mode 100644 index 0000000..9128437 --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/scanIntervalQuery.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: scanIntervalQuery +// ==================================================== + +export interface scanIntervalQuery_siteInfo { + __typename: "SiteInfo"; + /** + * How often automatic scans should be initiated in seconds + */ + periodicScanInterval: number; +} + +export interface scanIntervalQuery { + siteInfo: scanIntervalQuery_siteInfo; +} diff --git a/ui/src/Pages/SettingsPage/__generated__/setConcurrentWorkers.ts b/ui/src/Pages/SettingsPage/__generated__/setConcurrentWorkers.ts new file mode 100644 index 0000000..7c6207c --- /dev/null +++ b/ui/src/Pages/SettingsPage/__generated__/setConcurrentWorkers.ts @@ -0,0 +1,19 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: setConcurrentWorkers +// ==================================================== + +export interface setConcurrentWorkers { + /** + * Set max number of concurrent scanner jobs running at once + */ + setScannerConcurrentWorkers: number; +} + +export interface setConcurrentWorkersVariables { + workers: number; +} diff --git a/ui/src/Pages/SharePage/__generated__/SharePageToken.ts b/ui/src/Pages/SharePage/__generated__/SharePageToken.ts new file mode 100644 index 0000000..fd14709 --- /dev/null +++ b/ui/src/Pages/SharePage/__generated__/SharePageToken.ts @@ -0,0 +1,165 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: SharePageToken +// ==================================================== + +export interface SharePageToken_shareToken_album { + __typename: "Album"; + id: string; +} + +export interface SharePageToken_shareToken_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 SharePageToken_shareToken_media_downloads_mediaUrl { + __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; + /** + * The file size of the resource in bytes + */ + fileSize: number; +} + +export interface SharePageToken_shareToken_media_downloads { + __typename: "MediaDownload"; + title: string; + mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl; +} + +export interface SharePageToken_shareToken_media_highRes { + __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 SharePageToken_shareToken_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface SharePageToken_shareToken_media_exif { + __typename: "MediaEXIF"; + /** + * The model name of the camera + */ + camera: string | null; + /** + * The maker of the camera + */ + maker: string | null; + /** + * The name of the lens + */ + lens: string | null; + dateShot: any | null; + /** + * The exposure time of the image + */ + exposure: number | null; + /** + * The aperature stops of the image + */ + aperture: number | null; + /** + * The ISO setting of the image + */ + iso: number | null; + /** + * The focal length of the lens, when the image was taken + */ + focalLength: number | null; + /** + * A formatted description of the flash settings, when the image was taken + */ + flash: number | null; + /** + * An index describing the mode for adjusting the exposure of the image + */ + exposureProgram: number | null; +} + +export interface SharePageToken_shareToken_media { + __typename: "Media"; + id: string; + title: string; + type: MediaType; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: SharePageToken_shareToken_media_thumbnail | null; + downloads: SharePageToken_shareToken_media_downloads[]; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: SharePageToken_shareToken_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: SharePageToken_shareToken_media_videoWeb | null; + exif: SharePageToken_shareToken_media_exif | null; +} + +export interface SharePageToken_shareToken { + __typename: "ShareToken"; + token: string; + /** + * The album this token shares + */ + album: SharePageToken_shareToken_album | null; + /** + * The media this token shares + */ + media: SharePageToken_shareToken_media | null; +} + +export interface SharePageToken { + shareToken: SharePageToken_shareToken; +} + +export interface SharePageTokenVariables { + token: string; + password?: string | null; +} diff --git a/ui/src/Pages/SharePage/__generated__/ShareTokenValidatePassword.ts b/ui/src/Pages/SharePage/__generated__/ShareTokenValidatePassword.ts new file mode 100644 index 0000000..5f59ec2 --- /dev/null +++ b/ui/src/Pages/SharePage/__generated__/ShareTokenValidatePassword.ts @@ -0,0 +1,17 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: ShareTokenValidatePassword +// ==================================================== + +export interface ShareTokenValidatePassword { + shareTokenValidatePassword: boolean; +} + +export interface ShareTokenValidatePasswordVariables { + token: string; + password?: string | null; +} diff --git a/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts b/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts new file mode 100644 index 0000000..31c6b5e --- /dev/null +++ b/ui/src/Pages/SharePage/__generated__/shareAlbumQuery.ts @@ -0,0 +1,194 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: shareAlbumQuery +// ==================================================== + +export interface shareAlbumQuery_album_subAlbums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface shareAlbumQuery_album_subAlbums_thumbnail { + __typename: "Media"; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: shareAlbumQuery_album_subAlbums_thumbnail_thumbnail | null; +} + +export interface shareAlbumQuery_album_subAlbums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: shareAlbumQuery_album_subAlbums_thumbnail | null; +} + +export interface shareAlbumQuery_album_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 shareAlbumQuery_album_media_downloads_mediaUrl { + __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; + /** + * The file size of the resource in bytes + */ + fileSize: number; +} + +export interface shareAlbumQuery_album_media_downloads { + __typename: "MediaDownload"; + title: string; + mediaUrl: shareAlbumQuery_album_media_downloads_mediaUrl; +} + +export interface shareAlbumQuery_album_media_highRes { + __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 shareAlbumQuery_album_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface shareAlbumQuery_album_media_exif { + __typename: "MediaEXIF"; + /** + * The model name of the camera + */ + camera: string | null; + /** + * The maker of the camera + */ + maker: string | null; + /** + * The name of the lens + */ + lens: string | null; + dateShot: any | null; + /** + * The exposure time of the image + */ + exposure: number | null; + /** + * The aperature stops of the image + */ + aperture: number | null; + /** + * The ISO setting of the image + */ + iso: number | null; + /** + * The focal length of the lens, when the image was taken + */ + focalLength: number | null; + /** + * A formatted description of the flash settings, when the image was taken + */ + flash: number | null; + /** + * An index describing the mode for adjusting the exposure of the image + */ + exposureProgram: number | null; +} + +export interface shareAlbumQuery_album_media { + __typename: "Media"; + id: string; + title: string; + type: MediaType; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: shareAlbumQuery_album_media_thumbnail | null; + downloads: shareAlbumQuery_album_media_downloads[]; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: shareAlbumQuery_album_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: shareAlbumQuery_album_media_videoWeb | null; + exif: shareAlbumQuery_album_media_exif | null; +} + +export interface shareAlbumQuery_album { + __typename: "Album"; + id: string; + title: string; + /** + * The albums contained in this album + */ + subAlbums: shareAlbumQuery_album_subAlbums[]; + /** + * The media inside this album + */ + media: shareAlbumQuery_album_media[]; +} + +export interface shareAlbumQuery { + /** + * 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 + */ + album: shareAlbumQuery_album; +} + +export interface shareAlbumQueryVariables { + id: string; + token: string; + password?: string | null; + limit?: number | null; + offset?: number | null; +} diff --git a/ui/src/__generated__/adminQuery.ts b/ui/src/__generated__/adminQuery.ts new file mode 100644 index 0000000..232a958 --- /dev/null +++ b/ui/src/__generated__/adminQuery.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: adminQuery +// ==================================================== + +export interface adminQuery_myUser { + __typename: "User"; + admin: boolean; +} + +export interface adminQuery { + /** + * Information about the currently logged in user + */ + myUser: adminQuery_myUser; +} diff --git a/ui/src/__generated__/mapboxEnabledQuery.ts b/ui/src/__generated__/mapboxEnabledQuery.ts new file mode 100644 index 0000000..e666de4 --- /dev/null +++ b/ui/src/__generated__/mapboxEnabledQuery.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: mapboxEnabledQuery +// ==================================================== + +export interface mapboxEnabledQuery { + /** + * Get the mapbox api token, returns null if mapbox is not enabled + */ + mapboxToken: string | null; +} diff --git a/ui/src/__generated__/siteTranslation.ts b/ui/src/__generated__/siteTranslation.ts new file mode 100644 index 0000000..dfe5a7d --- /dev/null +++ b/ui/src/__generated__/siteTranslation.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { LanguageTranslation } from "./../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: siteTranslation +// ==================================================== + +export interface siteTranslation_myUserPreferences { + __typename: "UserPreferences"; + id: string; + language: LanguageTranslation | null; +} + +export interface siteTranslation { + myUserPreferences: siteTranslation_myUserPreferences; +} diff --git a/ui/src/components/__generated__/albumPathQuery.ts b/ui/src/components/__generated__/albumPathQuery.ts new file mode 100644 index 0000000..258c586 --- /dev/null +++ b/ui/src/components/__generated__/albumPathQuery.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: albumPathQuery +// ==================================================== + +export interface albumPathQuery_album_path { + __typename: "Album"; + id: string; + title: string; +} + +export interface albumPathQuery_album { + __typename: "Album"; + id: string; + path: albumPathQuery_album_path[]; +} + +export interface albumPathQuery { + /** + * 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 + */ + album: albumPathQuery_album; +} + +export interface albumPathQueryVariables { + id: string; +} diff --git a/ui/src/components/header/__generated__/searchQuery.ts b/ui/src/components/header/__generated__/searchQuery.ts new file mode 100644 index 0000000..434dd76 --- /dev/null +++ b/ui/src/components/header/__generated__/searchQuery.ts @@ -0,0 +1,76 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: searchQuery +// ==================================================== + +export interface searchQuery_search_albums_thumbnail_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface searchQuery_search_albums_thumbnail { + __typename: "Media"; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: searchQuery_search_albums_thumbnail_thumbnail | null; +} + +export interface searchQuery_search_albums { + __typename: "Album"; + id: string; + title: string; + /** + * An image in this album used for previewing this album + */ + thumbnail: searchQuery_search_albums_thumbnail | null; +} + +export interface searchQuery_search_media_thumbnail { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface searchQuery_search_media_album { + __typename: "Album"; + id: string; +} + +export interface searchQuery_search_media { + __typename: "Media"; + id: string; + title: string; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: searchQuery_search_media_thumbnail | null; + /** + * The album that holds the media + */ + album: searchQuery_search_media_album; +} + +export interface searchQuery_search { + __typename: "SearchResult"; + query: string; + albums: searchQuery_search_albums[]; + media: searchQuery_search_media[]; +} + +export interface searchQuery { + search: searchQuery_search; +} + +export interface searchQueryVariables { + query: string; +} diff --git a/ui/src/components/messages/__generated__/notificationSubscription.ts b/ui/src/components/messages/__generated__/notificationSubscription.ts new file mode 100644 index 0000000..b091a83 --- /dev/null +++ b/ui/src/components/messages/__generated__/notificationSubscription.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { NotificationType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL subscription operation: notificationSubscription +// ==================================================== + +export interface notificationSubscription_notification { + __typename: "Notification"; + key: string; + type: NotificationType; + header: string; + content: string; + progress: number | null; + positive: boolean; + negative: boolean; + /** + * Time in milliseconds before the notification will close + */ + timeout: number | null; +} + +export interface notificationSubscription { + notification: notificationSubscription_notification; +} diff --git a/ui/src/components/photoGallery/__generated__/markMediaFavorite.ts b/ui/src/components/photoGallery/__generated__/markMediaFavorite.ts new file mode 100644 index 0000000..2ab672b --- /dev/null +++ b/ui/src/components/photoGallery/__generated__/markMediaFavorite.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: markMediaFavorite +// ==================================================== + +export interface markMediaFavorite_favoriteMedia { + __typename: "Media"; + id: string; + favorite: boolean; +} + +export interface markMediaFavorite { + /** + * Mark or unmark a media as being a favorite + */ + favoriteMedia: markMediaFavorite_favoriteMedia | null; +} + +export interface markMediaFavoriteVariables { + mediaId: string; + favorite: boolean; +} diff --git a/ui/src/components/routes/AuthorizedRoute.js b/ui/src/components/routes/AuthorizedRoute.js index afc75a9..b0ccb2c 100644 --- a/ui/src/components/routes/AuthorizedRoute.js +++ b/ui/src/components/routes/AuthorizedRoute.js @@ -1,16 +1,9 @@ import React, { useEffect } from 'react' import PropTypes from 'prop-types' import { Route, Redirect } from 'react-router-dom' -import { gql, useLazyQuery } from '@apollo/client' +import { useLazyQuery } from '@apollo/client' import { authToken } from '../../helpers/authentication' - -export const ADMIN_QUERY = gql` - query adminQuery { - myUser { - admin - } - } -` +import { ADMIN_QUERY } from '../../Layout' export const useIsAdmin = (enabled = true) => { const [fetchAdminQuery, { data }] = useLazyQuery(ADMIN_QUERY) diff --git a/ui/src/components/routes/Routes.js b/ui/src/components/routes/Routes.tsx similarity index 100% rename from ui/src/components/routes/Routes.js rename to ui/src/components/routes/Routes.tsx diff --git a/ui/src/components/sidebar/__generated__/getAlbumSidebar.ts b/ui/src/components/sidebar/__generated__/getAlbumSidebar.ts new file mode 100644 index 0000000..e4e6604 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/getAlbumSidebar.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: getAlbumSidebar +// ==================================================== + +export interface getAlbumSidebar_album { + __typename: "Album"; + id: string; + title: string; +} + +export interface getAlbumSidebar { + /** + * 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 + */ + album: getAlbumSidebar_album; +} + +export interface getAlbumSidebarVariables { + id: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidbarGetAlbumShares.ts b/ui/src/components/sidebar/__generated__/sidbarGetAlbumShares.ts new file mode 100644 index 0000000..7e7fa27 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidbarGetAlbumShares.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: sidbarGetAlbumShares +// ==================================================== + +export interface sidbarGetAlbumShares_album_shares { + __typename: "ShareToken"; + id: string; + token: string; + /** + * Whether or not a password is needed to access the share + */ + hasPassword: boolean; +} + +export interface sidbarGetAlbumShares_album { + __typename: "Album"; + id: string; + shares: (sidbarGetAlbumShares_album_shares | null)[] | null; +} + +export interface sidbarGetAlbumShares { + /** + * 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 + */ + album: sidbarGetAlbumShares_album; +} + +export interface sidbarGetAlbumSharesVariables { + id: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidbarGetPhotoShares.ts b/ui/src/components/sidebar/__generated__/sidbarGetPhotoShares.ts new file mode 100644 index 0000000..400e27d --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidbarGetPhotoShares.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: sidbarGetPhotoShares +// ==================================================== + +export interface sidbarGetPhotoShares_media_shares { + __typename: "ShareToken"; + id: string; + token: string; + /** + * Whether or not a password is needed to access the share + */ + hasPassword: boolean; +} + +export interface sidbarGetPhotoShares_media { + __typename: "Media"; + id: string; + shares: sidbarGetPhotoShares_media_shares[]; +} + +export interface sidbarGetPhotoShares { + /** + * Get media by id, user must own the media or be admin. + * If valid tokenCredentials are provided, the media may be retrived without further authentication + */ + media: sidbarGetPhotoShares_media; +} + +export interface sidbarGetPhotoSharesVariables { + id: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarAlbumAddShare.ts b/ui/src/components/sidebar/__generated__/sidebarAlbumAddShare.ts new file mode 100644 index 0000000..50a80f5 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarAlbumAddShare.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebarAlbumAddShare +// ==================================================== + +export interface sidebarAlbumAddShare_shareAlbum { + __typename: "ShareToken"; + token: string; +} + +export interface sidebarAlbumAddShare { + /** + * Generate share token for album + */ + shareAlbum: sidebarAlbumAddShare_shareAlbum | null; +} + +export interface sidebarAlbumAddShareVariables { + id: string; + password?: string | null; + expire?: any | null; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarDownloadQuery.ts b/ui/src/components/sidebar/__generated__/sidebarDownloadQuery.ts new file mode 100644 index 0000000..dd67886 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarDownloadQuery.ts @@ -0,0 +1,52 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: sidebarDownloadQuery +// ==================================================== + +export interface sidebarDownloadQuery_media_downloads_mediaUrl { + __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; + /** + * The file size of the resource in bytes + */ + fileSize: number; +} + +export interface sidebarDownloadQuery_media_downloads { + __typename: "MediaDownload"; + title: string; + mediaUrl: sidebarDownloadQuery_media_downloads_mediaUrl; +} + +export interface sidebarDownloadQuery_media { + __typename: "Media"; + id: string; + downloads: sidebarDownloadQuery_media_downloads[]; +} + +export interface sidebarDownloadQuery { + /** + * Get media by id, user must own the media or be admin. + * If valid tokenCredentials are provided, the media may be retrived without further authentication + */ + media: sidebarDownloadQuery_media; +} + +export interface sidebarDownloadQueryVariables { + mediaId: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarPhoto.ts b/ui/src/components/sidebar/__generated__/sidebarPhoto.ts new file mode 100644 index 0000000..0766b5d --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarPhoto.ts @@ -0,0 +1,167 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: sidebarPhoto +// ==================================================== + +export interface sidebarPhoto_media_highRes { + __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 sidebarPhoto_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 sidebarPhoto_media_videoWeb { + __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 sidebarPhoto_media_videoMetadata { + __typename: "VideoMetadata"; + id: string; + width: number; + height: number; + duration: number; + codec: string | null; + framerate: number | null; + bitrate: string | null; + colorProfile: string | null; + audio: string | null; +} + +export interface sidebarPhoto_media_exif { + __typename: "MediaEXIF"; + id: string; + /** + * The model name of the camera + */ + camera: string | null; + /** + * The maker of the camera + */ + maker: string | null; + /** + * The name of the lens + */ + lens: string | null; + dateShot: any | null; + /** + * The exposure time of the image + */ + exposure: number | null; + /** + * The aperature stops of the image + */ + aperture: number | null; + /** + * The ISO setting of the image + */ + iso: number | null; + /** + * The focal length of the lens, when the image was taken + */ + focalLength: number | null; + /** + * A formatted description of the flash settings, when the image was taken + */ + flash: number | null; + /** + * An index describing the mode for adjusting the exposure of the image + */ + exposureProgram: number | null; +} + +export interface sidebarPhoto_media_faces_rectangle { + __typename: "FaceRectangle"; + minX: number; + maxX: number; + minY: number; + maxY: number; +} + +export interface sidebarPhoto_media_faces_faceGroup { + __typename: "FaceGroup"; + id: string; +} + +export interface sidebarPhoto_media_faces { + __typename: "ImageFace"; + id: string; + rectangle: sidebarPhoto_media_faces_rectangle | null; + faceGroup: sidebarPhoto_media_faces_faceGroup; +} + +export interface sidebarPhoto_media { + __typename: "Media"; + id: string; + title: string; + type: MediaType; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: sidebarPhoto_media_highRes | null; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: sidebarPhoto_media_thumbnail | null; + /** + * URL to get the video in a web format that can be played in the browser, will be null for photos + */ + videoWeb: sidebarPhoto_media_videoWeb | null; + videoMetadata: sidebarPhoto_media_videoMetadata | null; + exif: sidebarPhoto_media_exif | null; + faces: sidebarPhoto_media_faces[]; +} + +export interface sidebarPhoto { + /** + * Get media by id, user must own the media or be admin. + * If valid tokenCredentials are provided, the media may be retrived without further authentication + */ + media: sidebarPhoto_media; +} + +export interface sidebarPhotoVariables { + id: string; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarPhotoAddShare.ts b/ui/src/components/sidebar/__generated__/sidebarPhotoAddShare.ts new file mode 100644 index 0000000..8803f68 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarPhotoAddShare.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebarPhotoAddShare +// ==================================================== + +export interface sidebarPhotoAddShare_shareMedia { + __typename: "ShareToken"; + token: string; +} + +export interface sidebarPhotoAddShare { + /** + * Generate share token for media + */ + shareMedia: sidebarPhotoAddShare_shareMedia | null; +} + +export interface sidebarPhotoAddShareVariables { + id: string; + password?: string | null; + expire?: any | null; +} diff --git a/ui/src/components/sidebar/__generated__/sidebarProtectShare.ts b/ui/src/components/sidebar/__generated__/sidebarProtectShare.ts new file mode 100644 index 0000000..6ea497a --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebarProtectShare.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebarProtectShare +// ==================================================== + +export interface sidebarProtectShare_protectShareToken { + __typename: "ShareToken"; + token: string; + /** + * Whether or not a password is needed to access the share + */ + hasPassword: boolean; +} + +export interface sidebarProtectShare { + /** + * Set a password for a token, if null is passed for the password argument, the password will be cleared + */ + protectShareToken: sidebarProtectShare_protectShareToken | null; +} + +export interface sidebarProtectShareVariables { + token: string; + password?: string | null; +} diff --git a/ui/src/components/sidebar/__generated__/sidebareDeleteShare.ts b/ui/src/components/sidebar/__generated__/sidebareDeleteShare.ts new file mode 100644 index 0000000..59aaa32 --- /dev/null +++ b/ui/src/components/sidebar/__generated__/sidebareDeleteShare.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: sidebareDeleteShare +// ==================================================== + +export interface sidebareDeleteShare_deleteShareToken { + __typename: "ShareToken"; + token: string; +} + +export interface sidebareDeleteShare { + /** + * Delete a share token by it's token value + */ + deleteShareToken: sidebareDeleteShare_deleteShareToken | null; +} + +export interface sidebareDeleteShareVariables { + token: string; +} diff --git a/ui/src/components/timelineGallery/__generated__/myTimeline.ts b/ui/src/components/timelineGallery/__generated__/myTimeline.ts new file mode 100644 index 0000000..cfbaa97 --- /dev/null +++ b/ui/src/components/timelineGallery/__generated__/myTimeline.ts @@ -0,0 +1,94 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { MediaType } from "./../../../../__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: myTimeline +// ==================================================== + +export interface myTimeline_myTimeline_album { + __typename: "Album"; + id: string; + title: string; +} + +export interface myTimeline_myTimeline_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 myTimeline_myTimeline_media_highRes { + __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 myTimeline_myTimeline_media_videoWeb { + __typename: "MediaURL"; + /** + * URL for previewing the image + */ + url: string; +} + +export interface myTimeline_myTimeline_media { + __typename: "Media"; + id: string; + title: string; + type: MediaType; + /** + * URL to display the media in a smaller resolution + */ + thumbnail: myTimeline_myTimeline_media_thumbnail | null; + /** + * URL to display the photo in full resolution, will be null for videos + */ + highRes: myTimeline_myTimeline_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: myTimeline_myTimeline_media_videoWeb | null; + favorite: boolean; +} + +export interface myTimeline_myTimeline { + __typename: "TimelineGroup"; + album: myTimeline_myTimeline_album; + media: myTimeline_myTimeline_media[]; + mediaTotal: number; + date: any; +} + +export interface myTimeline { + myTimeline: myTimeline_myTimeline[]; +} + +export interface myTimelineVariables { + onlyFavorites?: boolean | null; + limit?: number | null; + offset?: number | null; +} diff --git a/ui/src/index.js b/ui/src/index.tsx similarity index 100% rename from ui/src/index.js rename to ui/src/index.tsx diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..b3854d6 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,72 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + // "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + // "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + "allowJs": true /* Allow javascript files to be compiled. */, + // "checkJs": true, /* Report errors in .js files. */ + "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */, + "resolveJsonModule": true, + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true /* Do not emit outputs. */, + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, + "strictNullChecks": true /* Enable strict null checks. */, + "strictFunctionTypes": true /* Enable strict checking of function types. */, + "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, + "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, + "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, + "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} From c5d2f3dc8b73f2cb20585f3ac8a82eabe1555d86 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 12 Apr 2021 16:06:06 +0200 Subject: [PATCH 18/28] Continue with typescript migration --- ui/.eslintrc.js | 27 +- ui/babel.config.js | 2 +- ui/build.mjs | 20 +- ui/package-lock.json | 864 ++++++++++++------ ui/package.json | 7 +- ui/src/@types/index.d.ts | 4 + ui/src/Layout.tsx | 2 +- ui/src/{apolloClient.js => apolloClient.ts} | 53 +- .../{AlbumTitle.js => AlbumTitle.tsx} | 24 +- .../{PaginateLoader.js => PaginateLoader.tsx} | 13 +- .../header/{Header.js => Header.tsx} | 0 .../header/{Searchbar.js => Searchbar.tsx} | 77 +- ui/src/components/messages/Messages.js | 8 +- ...scriptionsHook.js => SubscriptionsHook.ts} | 41 +- ...AuthorizedRoute.js => AuthorizedRoute.tsx} | 21 +- ui/src/helpers/{LazyLoad.js => LazyLoad.ts} | 23 +- .../{authentication.js => authentication.ts} | 6 +- ui/src/helpers/utils.js | 28 - ui/src/helpers/utils.ts | 39 + ...llPagination.js => useScrollPagination.ts} | 34 +- ...seURLParameters.js => useURLParameters.ts} | 6 +- ui/src/{localization.js => localization.ts} | 2 +- ui/tsconfig.json | 16 +- 23 files changed, 882 insertions(+), 435 deletions(-) create mode 100644 ui/src/@types/index.d.ts rename ui/src/{apolloClient.js => apolloClient.ts} (76%) rename ui/src/components/{AlbumTitle.js => AlbumTitle.tsx} (81%) rename ui/src/components/{PaginateLoader.js => PaginateLoader.tsx} (60%) rename ui/src/components/header/{Header.js => Header.tsx} (100%) rename ui/src/components/header/{Searchbar.js => Searchbar.tsx} (75%) rename ui/src/components/messages/{SubscriptionsHook.js => SubscriptionsHook.ts} (69%) rename ui/src/components/routes/{AuthorizedRoute.js => AuthorizedRoute.tsx} (71%) rename ui/src/helpers/{LazyLoad.js => LazyLoad.ts} (65%) rename ui/src/helpers/{authentication.js => authentication.ts} (75%) delete mode 100644 ui/src/helpers/utils.js create mode 100644 ui/src/helpers/utils.ts rename ui/src/hooks/{useScrollPagination.js => useScrollPagination.ts} (65%) rename ui/src/hooks/{useURLParameters.js => useURLParameters.ts} (79%) rename ui/src/{localization.js => localization.ts} (89%) diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index e5314a2..e14b0ea 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -1,10 +1,18 @@ module.exports = { + root: true, + parser: '@typescript-eslint/parser', env: { browser: true, es6: true, }, - ignorePatterns: ['**/*.ts', '**/*.tsx'], - extends: ['eslint:recommended', 'plugin:react/recommended'], + ignorePatterns: ['node_modules', 'dist'], + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], globals: { Atomics: 'readonly', SharedArrayBuffer: 'readonly', @@ -19,17 +27,20 @@ module.exports = { ecmaVersion: 2018, sourceType: 'module', }, - plugins: ['react', 'react-hooks'], + plugins: ['react', 'react-hooks', '@typescript-eslint'], rules: { - 'no-unused-vars': 'warn', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'warn', 'react/display-name': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', }, settings: { react: { version: 'detect', }, }, - parser: 'babel-eslint', + // parser: 'babel-eslint', overrides: [ Object.assign(require('eslint-plugin-jest').configs.recommended, { files: ['**/*.test.js'], @@ -43,5 +54,11 @@ module.exports = { } ), }), + { + files: ['**/*.js'], + rules: { + '@typescript-eslint/explicit-module-boundary-types': 'off', + }, + }, ], } diff --git a/ui/babel.config.js b/ui/babel.config.js index 43e0db3..5cdd249 100644 --- a/ui/babel.config.js +++ b/ui/babel.config.js @@ -2,7 +2,7 @@ module.exports = function (api) { const isTest = api.env('test') const isProduction = api.env('NODE_ENV') == 'production' - let presets = ['@babel/preset-react'] + let presets = ['@babel/preset-typescript', '@babel/preset-react'] let plugins = [] if (isTest) { diff --git a/ui/build.mjs b/ui/build.mjs index 3cab176..42f4006 100644 --- a/ui/build.mjs +++ b/ui/build.mjs @@ -23,7 +23,7 @@ const esbuildOptions = { entryPoints: ['src/index.tsx'], plugins: [ babel({ - filter: /photoview\/ui\/src\/.*\.js$/, + filter: /photoview\/ui\/src\/.*\.(js|tsx?)$/, }), ], publicPath: process.env.UI_PUBLIC_URL || '/', @@ -66,25 +66,25 @@ if (watchMode) { open: false, }) - bs.watch('src/**/*.js').on('change', async args => { + bs.watch('src/**/*.@(js|tsx|ts)').on('change', async args => { console.log('reloading', args) builderPromise = (await builderPromise).rebuild() bs.reload(args) }) } else { - const esbuildPromise = esbuild - .build(esbuildOptions) - .then(() => console.log('esbuild done')) + const build = async () => { + await esbuild.build(esbuildOptions) - const workboxPromise = workboxBuild - .generateSW({ + console.log('esbuild done') + + await workboxBuild.generateSW({ globDirectory: 'dist/', globPatterns: ['**/*.{png,svg,woff2,ttf,eot,woff,js,ico,html,json,css}'], swDest: 'dist/service-worker.js', }) - .then(() => console.log('workbox done')) - Promise.all([esbuildPromise, workboxPromise]).then(() => + console.log('workbox done') console.log('build complete') - ) + } + build() } diff --git a/ui/package-lock.json b/ui/package-lock.json index ef8a704..2298621 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -15,6 +15,7 @@ "@babel/plugin-transform-runtime": "^7.13.15", "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", + "@babel/preset-typescript": "^7.13.0", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "babel-plugin-graphql-tag": "^3.2.0", @@ -60,6 +61,10 @@ "@types/react-helmet": "^6.1.1", "@types/react-router-dom": "^5.1.7", "@types/styled-components": "^5.1.9", + "@types/url-join": "^4.0.0", + "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/parser": "^4.21.0", + "eslint-config-prettier": "^8.1.0", "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^10.5.4", @@ -769,6 +774,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", + "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz", @@ -1192,6 +1208,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.13.0.tgz", + "integrity": "sha512-elQEwluzaU8R8dbVuW2Q2Y8Nznf7hnjM7+DSCd14Lo5fF63C9qNLbwZYbmZrtV9/ySpSUpkRpQXvJb6xyu4hCQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-typescript": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz", @@ -1330,6 +1359,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz", + "integrity": "sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-transform-typescript": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.13.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", @@ -2344,31 +2386,31 @@ } }, "node_modules/@nodelib/fs.scandir": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", - "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", "dependencies": { - "@nodelib/fs.stat": "2.0.3", + "@nodelib/fs.stat": "2.0.4", "run-parallel": "^1.1.9" }, "engines": { "node": ">= 8" } }, - "node_modules/@nodelib/fs.scandir/node_modules/@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", - "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", "dependencies": { - "@nodelib/fs.scandir": "2.1.3", + "@nodelib/fs.scandir": "2.1.4", "fastq": "^1.6.0" }, "engines": { @@ -2882,6 +2924,12 @@ "@types/jest": "*" } }, + "node_modules/@types/url-join": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/url-join/-/url-join-4.0.0.tgz", + "integrity": "sha512-awrJu8yML4E/xTwr2EMatC+HBnHGoDxc2+ImA9QyeUELI1S7dOCIZcyjki1rkwoA8P2D2NVgLAJLjnclkdLtAw==", + "dev": true + }, "node_modules/@types/yargs": { "version": "15.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", @@ -2900,64 +2948,164 @@ "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.1.tgz", "integrity": "sha512-wmk0xQI6Yy7Fs/il4EpOcflG4uonUpYGqvZARESLc2oy4u69fkatFLbJOeW4Q6awO15P4rduAe6xkwHevpXcUQ==" }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.6.0.tgz", - "integrity": "sha512-pnh6Beh2/4xjJVNL+keP49DFHk3orDHHFylSp3WEjtgW3y1U+6l+jNnJrGlbs6qhAz5z96aFmmbUyKhunXKvKw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz", + "integrity": "sha512-FPUyCPKZbVGexmbCFI3EQHzCZdy2/5f+jv6k2EDljGdXSRc0cKvbndd2nHZkSLqCNOPk0jB6lGzwIkglXcYVsQ==", + "devOptional": true, "dependencies": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.6.0", - "@typescript-eslint/types": "4.6.0", - "@typescript-eslint/typescript-estree": "4.6.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.6.0.tgz", - "integrity": "sha512-uZx5KvStXP/lwrMrfQQwDNvh2ppiXzz5TmyTVHb+5TfZ3sUP7U1onlz3pjoWrK9konRyFe1czyxObWTly27Ang==", - "dependencies": { - "@typescript-eslint/types": "4.6.0", - "@typescript-eslint/visitor-keys": "4.6.0" - }, - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.6.0.tgz", - "integrity": "sha512-5FAgjqH68SfFG4UTtIFv+rqYJg0nLjfkjD0iv+5O27a0xEeNZ5rZNDvFGZDizlCD1Ifj7MAbSW2DPMrf0E9zjA==", - "engines": { - "node": "^8.10.0 || ^10.13.0 || >=11.10.1" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.0.tgz", - "integrity": "sha512-s4Z9qubMrAo/tw0CbN0IN4AtfwuehGXVZM0CHNMdfYMGBDhPdwTEpBrecwhP7dRJu6d9tT9ECYNaWDHvlFSngA==", - "dependencies": { - "@typescript-eslint/types": "4.6.0", - "@typescript-eslint/visitor-keys": "4.6.0", + "@typescript-eslint/experimental-utils": "4.21.0", + "@typescript-eslint/scope-manager": "4.21.0", "debug": "^4.1.1", - "globby": "^11.0.1", - "is-glob": "^4.0.1", + "functional-red-black-tree": "^1.0.1", "lodash": "^4.17.15", + "regexpp": "^3.0.0", "semver": "^7.3.2", "tsutils": "^3.17.1" }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "devOptional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz", + "integrity": "sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA==", + "dependencies": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.21.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/typescript-estree": "4.21.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.21.0.tgz", + "integrity": "sha512-eyNf7QmE5O/l1smaQgN0Lj2M/1jOuNg2NrBm1dqqQN0sVngTLyw8tdCbih96ixlhbF1oINoN8fDCyEH9SjLeIA==", + "devOptional": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.21.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/typescript-estree": "4.21.0", + "debug": "^4.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz", + "integrity": "sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw==", + "dependencies": { + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.21.0.tgz", + "integrity": "sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w==", + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz", + "integrity": "sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w==", + "dependencies": { + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" }, @@ -2966,15 +3114,19 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.0.tgz", - "integrity": "sha512-38Aa9Ztl0XyFPVzmutHXqDMCu15Xx8yKvUo38Gu3GhsuckCh3StPI5t2WIO9LHEsOH7MLmlGfKUisU8eW1Sjhg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz", + "integrity": "sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w==", "dependencies": { - "@typescript-eslint/types": "4.6.0", + "@typescript-eslint/types": "4.21.0", "eslint-visitor-keys": "^2.0.0" }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { @@ -5173,6 +5325,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz", + "integrity": "sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-jest": { "version": "24.3.5", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.5.tgz", @@ -5934,6 +6098,75 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/fast-glob/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5945,9 +6178,9 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "node_modules/fastq": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", - "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", "dependencies": { "reusify": "^1.0.4" } @@ -6291,9 +6524,9 @@ } }, "node_modules/globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -6304,52 +6537,9 @@ }, "engines": { "node": ">=10" - } - }, - "node_modules/globby/node_modules/@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/globby/node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/globby/node_modules/fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/globby/node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby/node_modules/ignore": { @@ -6360,37 +6550,6 @@ "node": ">= 4" } }, - "node_modules/globby/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/globby/node_modules/micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/globby/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -9981,6 +10140,17 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", @@ -10773,11 +10943,14 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", "engines": { "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pirates": { @@ -10981,6 +11154,25 @@ "node": ">=0.6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/quickselect": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", @@ -11639,9 +11831,26 @@ } }, "node_modules/run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } }, "node_modules/rw": { "version": "1.3.3", @@ -13894,6 +14103,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/yaml": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", @@ -14543,6 +14757,14 @@ "@babel/helper-plugin-utils": "^7.12.13" } }, + "@babel/plugin-syntax-typescript": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", + "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz", @@ -14863,6 +15085,16 @@ "@babel/helper-plugin-utils": "^7.12.13" } }, + "@babel/plugin-transform-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.13.0.tgz", + "integrity": "sha512-elQEwluzaU8R8dbVuW2Q2Y8Nznf7hnjM7+DSCd14Lo5fF63C9qNLbwZYbmZrtV9/ySpSUpkRpQXvJb6xyu4hCQ==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-typescript": "^7.12.13" + } + }, "@babel/plugin-transform-unicode-escapes": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz", @@ -14988,6 +15220,16 @@ "@babel/plugin-transform-react-pure-annotations": "^7.12.1" } }, + "@babel/preset-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz", + "integrity": "sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==", + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-transform-typescript": "^7.13.0" + } + }, "@babel/runtime": { "version": "7.13.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", @@ -15825,27 +16067,25 @@ "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" }, "@nodelib/fs.scandir": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", - "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", "requires": { - "@nodelib/fs.stat": "2.0.3", + "@nodelib/fs.stat": "2.0.4", "run-parallel": "^1.1.9" - }, - "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==" - } } }, + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==" + }, "@nodelib/fs.walk": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", - "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", "requires": { - "@nodelib/fs.scandir": "2.1.3", + "@nodelib/fs.scandir": "2.1.4", "fastq": "^1.6.0" } }, @@ -16298,6 +16538,12 @@ "@types/jest": "*" } }, + "@types/url-join": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/url-join/-/url-join-4.0.0.tgz", + "integrity": "sha512-awrJu8yML4E/xTwr2EMatC+HBnHGoDxc2+ImA9QyeUELI1S7dOCIZcyjki1rkwoA8P2D2NVgLAJLjnclkdLtAw==", + "dev": true + }, "@types/yargs": { "version": "15.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", @@ -16316,61 +16562,102 @@ "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.1.tgz", "integrity": "sha512-wmk0xQI6Yy7Fs/il4EpOcflG4uonUpYGqvZARESLc2oy4u69fkatFLbJOeW4Q6awO15P4rduAe6xkwHevpXcUQ==" }, - "@typescript-eslint/experimental-utils": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.6.0.tgz", - "integrity": "sha512-pnh6Beh2/4xjJVNL+keP49DFHk3orDHHFylSp3WEjtgW3y1U+6l+jNnJrGlbs6qhAz5z96aFmmbUyKhunXKvKw==", + "@typescript-eslint/eslint-plugin": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.21.0.tgz", + "integrity": "sha512-FPUyCPKZbVGexmbCFI3EQHzCZdy2/5f+jv6k2EDljGdXSRc0cKvbndd2nHZkSLqCNOPk0jB6lGzwIkglXcYVsQ==", + "devOptional": true, "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.6.0", - "@typescript-eslint/types": "4.6.0", - "@typescript-eslint/typescript-estree": "4.6.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" - } - }, - "@typescript-eslint/scope-manager": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.6.0.tgz", - "integrity": "sha512-uZx5KvStXP/lwrMrfQQwDNvh2ppiXzz5TmyTVHb+5TfZ3sUP7U1onlz3pjoWrK9konRyFe1czyxObWTly27Ang==", - "requires": { - "@typescript-eslint/types": "4.6.0", - "@typescript-eslint/visitor-keys": "4.6.0" - } - }, - "@typescript-eslint/types": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.6.0.tgz", - "integrity": "sha512-5FAgjqH68SfFG4UTtIFv+rqYJg0nLjfkjD0iv+5O27a0xEeNZ5rZNDvFGZDizlCD1Ifj7MAbSW2DPMrf0E9zjA==" - }, - "@typescript-eslint/typescript-estree": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.0.tgz", - "integrity": "sha512-s4Z9qubMrAo/tw0CbN0IN4AtfwuehGXVZM0CHNMdfYMGBDhPdwTEpBrecwhP7dRJu6d9tT9ECYNaWDHvlFSngA==", - "requires": { - "@typescript-eslint/types": "4.6.0", - "@typescript-eslint/visitor-keys": "4.6.0", + "@typescript-eslint/experimental-utils": "4.21.0", + "@typescript-eslint/scope-manager": "4.21.0", "debug": "^4.1.1", - "globby": "^11.0.1", - "is-glob": "^4.0.1", + "functional-red-black-tree": "^1.0.1", "lodash": "^4.17.15", + "regexpp": "^3.0.0", "semver": "^7.3.2", "tsutils": "^3.17.1" }, "dependencies": { "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "devOptional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.21.0.tgz", + "integrity": "sha512-cEbgosW/tUFvKmkg3cU7LBoZhvUs+ZPVM9alb25XvR0dal4qHL3SiUqHNrzoWSxaXA9gsifrYrS1xdDV6w/gIA==", + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.21.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/typescript-estree": "4.21.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.21.0.tgz", + "integrity": "sha512-eyNf7QmE5O/l1smaQgN0Lj2M/1jOuNg2NrBm1dqqQN0sVngTLyw8tdCbih96ixlhbF1oINoN8fDCyEH9SjLeIA==", + "devOptional": true, + "requires": { + "@typescript-eslint/scope-manager": "4.21.0", + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/typescript-estree": "4.21.0", + "debug": "^4.1.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.21.0.tgz", + "integrity": "sha512-kfOjF0w1Ix7+a5T1knOw00f7uAP9Gx44+OEsNQi0PvvTPLYeXJlsCJ4tYnDj5PQEYfpcgOH5yBlw7K+UEI9Agw==", + "requires": { + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0" + } + }, + "@typescript-eslint/types": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.21.0.tgz", + "integrity": "sha512-+OQaupjGVVc8iXbt6M1oZMwyKQNehAfLYJJ3SdvnofK2qcjfor9pEM62rVjBknhowTkh+2HF+/KdRAc/wGBN2w==" + }, + "@typescript-eslint/typescript-estree": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.21.0.tgz", + "integrity": "sha512-ZD3M7yLaVGVYLw4nkkoGKumb7Rog7QID9YOWobFDMQKNl+vPxqVIW/uDk+MDeGc+OHcoG2nJ2HphwiPNajKw3w==", + "requires": { + "@typescript-eslint/types": "4.21.0", + "@typescript-eslint/visitor-keys": "4.21.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } } } }, "@typescript-eslint/visitor-keys": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.0.tgz", - "integrity": "sha512-38Aa9Ztl0XyFPVzmutHXqDMCu15Xx8yKvUo38Gu3GhsuckCh3StPI5t2WIO9LHEsOH7MLmlGfKUisU8eW1Sjhg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.21.0.tgz", + "integrity": "sha512-dH22dROWGi5Z6p+Igc8bLVLmwy7vEe8r+8c+raPQU0LxgogPUrRAtRGtvBWmlr9waTu3n+QLt/qrS/hWzk1x5w==", "requires": { - "@typescript-eslint/types": "4.6.0", + "@typescript-eslint/types": "4.21.0", "eslint-visitor-keys": "^2.0.0" }, "dependencies": { @@ -18296,6 +18583,13 @@ } } }, + "eslint-config-prettier": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz", + "integrity": "sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==", + "dev": true, + "requires": {} + }, "eslint-plugin-jest": { "version": "24.3.5", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.3.5.tgz", @@ -18720,6 +19014,59 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -18731,9 +19078,9 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fastq": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", - "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", "requires": { "reusify": "^1.0.4" } @@ -19005,9 +19352,9 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", "requires": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -19017,66 +19364,10 @@ "slash": "^3.0.0" }, "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==" - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, "ignore": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } } } }, @@ -21898,6 +22189,14 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", @@ -22521,9 +22820,9 @@ "dev": true }, "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==" }, "pirates": { "version": "4.0.1", @@ -22682,6 +22981,11 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, "quickselect": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", @@ -23227,9 +23531,12 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" }, "run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "requires": { + "queue-microtask": "^1.2.2" + } }, "rw": { "version": "1.3.3", @@ -25118,6 +25425,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "yaml": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", diff --git a/ui/package.json b/ui/package.json index 24d6612..04be30a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,6 +15,7 @@ "@babel/plugin-transform-runtime": "^7.13.15", "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", + "@babel/preset-typescript": "^7.13.0", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "babel-plugin-graphql-tag": "^3.2.0", @@ -56,7 +57,7 @@ "start": "node --experimental-modules build.mjs watch", "build": "NODE_ENV=production node --experimental-modules build.mjs", "test": "npm run lint && npm run jest", - "lint": "eslint ./src --max-warnings 0 --cache", + "lint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js", "jest": "jest", "genSchemaTypes": "npx apollo client:codegen --target=typescript", "prepare": "(cd .. && npx husky install)" @@ -69,6 +70,10 @@ "@types/react-helmet": "^6.1.1", "@types/react-router-dom": "^5.1.7", "@types/styled-components": "^5.1.9", + "@types/url-join": "^4.0.0", + "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/parser": "^4.21.0", + "eslint-config-prettier": "^8.1.0", "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^10.5.4", diff --git a/ui/src/@types/index.d.ts b/ui/src/@types/index.d.ts new file mode 100644 index 0000000..d41bc37 --- /dev/null +++ b/ui/src/@types/index.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const src: string + export default src +} diff --git a/ui/src/Layout.tsx b/ui/src/Layout.tsx index fe11940..bcb2241 100644 --- a/ui/src/Layout.tsx +++ b/ui/src/Layout.tsx @@ -80,7 +80,7 @@ const SideButtonLink = styled(NavLink)` ` type SideButtonProps = { - children: any + children: ReactChild | ReactChild[] to: string exact: boolean } diff --git a/ui/src/apolloClient.js b/ui/src/apolloClient.ts similarity index 76% rename from ui/src/apolloClient.js rename to ui/src/apolloClient.ts index c353ad9..c239aa8 100644 --- a/ui/src/apolloClient.js +++ b/ui/src/apolloClient.ts @@ -4,6 +4,8 @@ import { split, ApolloLink, HttpLink, + ServerError, + FieldMergeFunction, } from '@apollo/client' import { getMainDefinition } from '@apollo/client/utilities' import { onError } from '@apollo/client/link/error' @@ -12,6 +14,7 @@ import { WebSocketLink } from '@apollo/client/link/ws' import urlJoin from 'url-join' import { clearTokenCookie } from './helpers/authentication' import { MessageState } from './components/messages/Messages' +import { Message } from './components/messages/SubscriptionsHook' export const GRAPHQL_ENDPOINT = process.env.PHOTOVIEW_API_ENDPOINT ? urlJoin(process.env.PHOTOVIEW_API_ENDPOINT, '/graphql') @@ -26,12 +29,12 @@ console.log('GRAPHQL ENDPOINT', GRAPHQL_ENDPOINT) const apiProtocol = new URL(GRAPHQL_ENDPOINT).protocol -let websocketUri = new URL(GRAPHQL_ENDPOINT) +const websocketUri = new URL(GRAPHQL_ENDPOINT) websocketUri.protocol = apiProtocol === 'https:' ? 'wss:' : 'ws:' const wsLink = new WebSocketLink({ - uri: websocketUri, - credentials: 'include', + uri: websocketUri.toString(), + // credentials: 'include', }) const link = split( @@ -48,7 +51,7 @@ const link = split( ) const linkError = onError(({ graphQLErrors, networkError }) => { - let errorMessages = [] + const errorMessages = [] if (graphQLErrors) { graphQLErrors.map(({ message, locations, path }) => @@ -82,7 +85,7 @@ const linkError = onError(({ graphQLErrors, networkError }) => { console.log(`[Network error]: ${JSON.stringify(networkError)}`) clearTokenCookie() - const errors = networkError.result.errors + const errors = (networkError as ServerError).result.errors if (errors.length == 1) { errorMessages.push({ @@ -92,7 +95,9 @@ const linkError = onError(({ graphQLErrors, networkError }) => { } else if (errors.length > 1) { errorMessages.push({ header: 'Multiple server errors', - content: `Received ${graphQLErrors.length} errors from the server. You are being logged out in an attempt to recover.`, + content: `Received ${ + graphQLErrors?.length || 0 + } errors from the server. You are being logged out in an attempt to recover.`, }) } } @@ -106,26 +111,32 @@ const linkError = onError(({ graphQLErrors, networkError }) => { ...msg, }, })) - MessageState.set(messages => [...messages, ...newMessages]) + MessageState.set((messages: Message[]) => [...messages, ...newMessages]) } }) +type PaginateCacheType = { + keyArgs: string[] + merge: FieldMergeFunction +} + // Modified version of Apollo's offsetLimitPagination() -const paginateCache = keyArgs => ({ - keyArgs, - merge(existing, incoming, { args, fieldName }) { - const merged = existing ? existing.slice(0) : [] - if (args?.paginate) { - const { offset = 0 } = args.paginate - for (let i = 0; i < incoming.length; ++i) { - merged[offset + i] = incoming[i] +const paginateCache = (keyArgs: string[]) => + ({ + keyArgs, + merge(existing, incoming, { args, fieldName }) { + const merged = existing ? existing.slice(0) : [] + if (args?.paginate) { + const { offset = 0 } = args.paginate + for (let i = 0; i < incoming.length; ++i) { + merged[offset + i] = incoming[i] + } + } else { + throw new Error(`Paginate argument is missing for query: ${fieldName}`) } - } else { - throw new Error(`Paginate argument is missing for query: ${fieldName}`) - } - return merged - }, -}) + return merged + }, + } as PaginateCacheType) const memoryCache = new InMemoryCache({ typePolicies: { diff --git a/ui/src/components/AlbumTitle.js b/ui/src/components/AlbumTitle.tsx similarity index 81% rename from ui/src/components/AlbumTitle.js rename to ui/src/components/AlbumTitle.tsx index f4a6872..b7fd32b 100644 --- a/ui/src/components/AlbumTitle.js +++ b/ui/src/components/AlbumTitle.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useContext } from 'react' import PropTypes from 'prop-types' -import { Breadcrumb } from 'semantic-ui-react' +import { Breadcrumb, IconProps } from 'semantic-ui-react' import { Link } from 'react-router-dom' import styled from 'styled-components' import { Icon } from 'semantic-ui-react' @@ -8,6 +8,7 @@ import { SidebarContext } from './sidebar/Sidebar' import AlbumSidebar from './sidebar/AlbumSidebar' import { useLazyQuery, gql } from '@apollo/client' import { authToken } from '../helpers/authentication' +import { albumPathQuery } from './__generated__/albumPathQuery' const Header = styled.h1` margin: 24px 0 8px 0 !important; @@ -32,7 +33,7 @@ const StyledIcon = styled(Icon)` } ` -const SettingsIcon = props => { +const SettingsIcon = (props: IconProps) => { return } @@ -48,8 +49,18 @@ const ALBUM_PATH_QUERY = gql` } ` -const AlbumTitle = ({ album, disableLink = false }) => { - const [fetchPath, { data: pathData }] = useLazyQuery(ALBUM_PATH_QUERY) +type AlbumTitleProps = { + album: { + id: string + title: string + } + disableLink: boolean +} + +const AlbumTitle = ({ album, disableLink = false }: AlbumTitleProps) => { + const [fetchPath, { data: pathData }] = useLazyQuery( + ALBUM_PATH_QUERY + ) const { updateSidebar } = useContext(SidebarContext) useEffect(() => { @@ -68,10 +79,7 @@ const AlbumTitle = ({ album, disableLink = false }) => { let title = {album.title} - let path = [] - if (pathData) { - path = pathData.album.path - } + const path = pathData?.album.path || [] const breadcrumbSections = path .slice() diff --git a/ui/src/components/PaginateLoader.js b/ui/src/components/PaginateLoader.tsx similarity index 60% rename from ui/src/components/PaginateLoader.js rename to ui/src/components/PaginateLoader.tsx index 19e6615..4c9ad2b 100644 --- a/ui/src/components/PaginateLoader.js +++ b/ui/src/components/PaginateLoader.tsx @@ -1,8 +1,12 @@ import React from 'react' -import PropTypes from 'prop-types' import { Loader } from 'semantic-ui-react' -const PaginateLoader = ({ active, text }) => ( +type PaginateLoaderProps = { + active: boolean + text: string +} + +const PaginateLoader = ({ active, text }: PaginateLoaderProps) => ( ( ) -PaginateLoader.propTypes = { - active: PropTypes.bool, - text: PropTypes.string, -} - export default PaginateLoader diff --git a/ui/src/components/header/Header.js b/ui/src/components/header/Header.tsx similarity index 100% rename from ui/src/components/header/Header.js rename to ui/src/components/header/Header.tsx diff --git a/ui/src/components/header/Searchbar.js b/ui/src/components/header/Searchbar.tsx similarity index 75% rename from ui/src/components/header/Searchbar.js rename to ui/src/components/header/Searchbar.tsx index 2f7886b..4d3d227 100644 --- a/ui/src/components/header/Searchbar.js +++ b/ui/src/components/header/Searchbar.tsx @@ -1,11 +1,15 @@ import React, { useState, useRef, useEffect } from 'react' -import PropTypes from 'prop-types' import styled from 'styled-components' import { useLazyQuery, gql } from '@apollo/client' -import { debounce } from '../../helpers/utils' +import { debounce, DebouncedFn } from '../../helpers/utils' import { ProtectedImage } from '../photoGallery/ProtectedMedia' import { NavLink } from 'react-router-dom' import { useTranslation } from 'react-i18next' +import { + searchQuery, + searchQuery_search_albums, + searchQuery_search_media, +} from './__generated__/searchQuery' const Container = styled.div` height: 60px; @@ -30,7 +34,7 @@ const SearchField = styled.input` } ` -const Results = styled.div` +const Results = styled.div<{ show: boolean }>` display: ${({ show }) => (show ? 'block' : 'none')}; position: absolute; width: 100%; @@ -79,27 +83,29 @@ const SEARCH_QUERY = gql` const SearchBar = () => { const { t } = useTranslation() - const [fetchSearches, fetchResult] = useLazyQuery(SEARCH_QUERY) + const [fetchSearches, fetchResult] = useLazyQuery(SEARCH_QUERY) const [query, setQuery] = useState('') const [fetched, setFetched] = useState(false) - let debouncedFetch = useRef(null) + type QueryFn = (query: string) => void + + const debouncedFetch = useRef>(null) useEffect(() => { - debouncedFetch.current = debounce(query => { + debouncedFetch.current = debounce(query => { fetchSearches({ variables: { query } }) setFetched(true) }, 250) return () => { - debouncedFetch.current.cancel() + debouncedFetch.current?.cancel() } }, []) - const fetchEvent = e => { + const fetchEvent = (e: React.ChangeEvent) => { e.persist() setQuery(e.target.value) - if (e.target.value.trim() != '') { + if (e.target.value.trim() != '' && debouncedFetch.current) { debouncedFetch.current(e.target.value.trim()) } else { setFetched(false) @@ -108,7 +114,12 @@ const SearchBar = () => { let results = null if (query.trim().length > 0 && fetched) { - results = + results = ( + + ) } return ( @@ -128,17 +139,21 @@ const ResultTitle = styled.h1` margin: 12px 0 0.25rem; ` -const SearchResults = ({ result }) => { - const { t } = useTranslation() - const { data, loading } = result - const query = data && data.search.query +type SearchResultsProps = { + searchData?: searchQuery + loading: boolean +} - const media = (data && data.search.media) || [] - const albums = (data && data.search.albums) || [] +const SearchResults = ({ searchData, loading }: SearchResultsProps) => { + const { t } = useTranslation() + const query = searchData?.search.query || '' + + const media = searchData?.search.media || [] + const albums = searchData?.search.albums || [] let message = null if (loading) message = t('header.search.loading', 'Loading results...') - else if (data && media.length == 0 && albums.length == 0) + else if (searchData && media.length == 0 && albums.length == 0) message = t('header.search.no_results', 'No results found') const albumElements = albums.map(album => ( @@ -155,7 +170,7 @@ const SearchResults = ({ result }) => { // Prevent input blur event e.preventDefault() }} - show={data} + show={!!searchData} > {message} {albumElements.length > 0 && ( @@ -174,10 +189,6 @@ const SearchResults = ({ result }) => { ) } -SearchResults.propTypes = { - result: PropTypes.object, -} - const RowLink = styled(NavLink)` display: flex; align-items: center; @@ -205,31 +216,31 @@ const RowTitle = styled.span` padding-left: 8px; ` -const PhotoRow = ({ query, media }) => ( +type PhotoRowArgs = { + query: string + media: searchQuery_search_media +} + +const PhotoRow = ({ query, media }: PhotoRowArgs) => ( {searchHighlighted(query, media.title)} ) -PhotoRow.propTypes = { - query: PropTypes.string.isRequired, - media: PropTypes.object.isRequired, +type AlbumRowArgs = { + query: string + album: searchQuery_search_albums } -const AlbumRow = ({ query, album }) => ( +const AlbumRow = ({ query, album }: AlbumRowArgs) => ( {searchHighlighted(query, album.title)} ) -AlbumRow.propTypes = { - query: PropTypes.string.isRequired, - album: PropTypes.object.isRequired, -} - -const searchHighlighted = (query, text) => { +const searchHighlighted = (query: string, text: string) => { const i = text.toLowerCase().indexOf(query.toLowerCase()) if (i == -1) { diff --git a/ui/src/components/messages/Messages.js b/ui/src/components/messages/Messages.js index d16ae24..894c3f5 100644 --- a/ui/src/components/messages/Messages.js +++ b/ui/src/components/messages/Messages.js @@ -17,9 +17,11 @@ const Container = styled.div` } ` -export let MessageState = { - set: null, - get: null, +export const MessageState = { + set: fn => { + console.warn('set function is not defined yet, called with', fn) + }, + get: [], add: message => { MessageState.set(messages => { const newMessages = messages.filter(msg => msg.key != message.key) diff --git a/ui/src/components/messages/SubscriptionsHook.js b/ui/src/components/messages/SubscriptionsHook.ts similarity index 69% rename from ui/src/components/messages/SubscriptionsHook.js rename to ui/src/components/messages/SubscriptionsHook.ts index 5b52737..de1b287 100644 --- a/ui/src/components/messages/SubscriptionsHook.js +++ b/ui/src/components/messages/SubscriptionsHook.ts @@ -1,7 +1,9 @@ +import { notificationSubscription } from './__generated__/notificationSubscription' import PropTypes from 'prop-types' import { useEffect } from 'react' import { useSubscription, gql } from '@apollo/client' import { authToken } from '../../helpers/authentication' +import { NotificationType } from '../../../__generated__/globalTypes' const notificationSubscription = gql` subscription notificationSubscription { @@ -18,14 +20,37 @@ const notificationSubscription = gql` } ` -let messageTimeoutHandles = new Map() +const messageTimeoutHandles = new Map() -const SubscriptionsHook = ({ messages, setMessages }) => { +export interface Message { + key: string + type: NotificationType + timeout?: number + props: { + header: string + content: string + negative?: boolean + positive?: boolean + percent?: number + } +} + +type SubscriptionHookProps = { + messages: Message[] + setMessages: React.Dispatch> +} + +const SubscriptionsHook = ({ + messages, + setMessages, +}: SubscriptionHookProps) => { if (!authToken()) { return null } - const { data, error } = useSubscription(notificationSubscription) + const { data, error } = useSubscription( + notificationSubscription + ) useEffect(() => { if (error) { @@ -33,7 +58,7 @@ const SubscriptionsHook = ({ messages, setMessages }) => { ...state, { key: Math.random().toString(26), - type: 'message', + type: NotificationType.Message, props: { header: 'Network error', content: error.message, @@ -54,16 +79,16 @@ const SubscriptionsHook = ({ messages, setMessages }) => { return } - const newNotification = { + const newNotification: Message = { key: msg.key, - type: msg.type.toLowerCase(), - timeout: msg.timeout, + type: msg.type, + timeout: msg.timeout || undefined, props: { header: msg.header, content: msg.content, negative: msg.negative, positive: msg.positive, - percent: msg.progress, + percent: msg.progress || undefined, }, } diff --git a/ui/src/components/routes/AuthorizedRoute.js b/ui/src/components/routes/AuthorizedRoute.tsx similarity index 71% rename from ui/src/components/routes/AuthorizedRoute.js rename to ui/src/components/routes/AuthorizedRoute.tsx index b0ccb2c..2ba606e 100644 --- a/ui/src/components/routes/AuthorizedRoute.js +++ b/ui/src/components/routes/AuthorizedRoute.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from 'react' -import PropTypes from 'prop-types' +import React, { ReactChild, useEffect } from 'react' +import PropTypes, { ReactComponentLike } from 'prop-types' import { Route, Redirect } from 'react-router-dom' import { useLazyQuery } from '@apollo/client' import { authToken } from '../../helpers/authentication' @@ -21,22 +21,31 @@ export const useIsAdmin = (enabled = true) => { return data?.myUser?.admin } -export const Authorized = ({ children }) => { +export const Authorized = ({ children }: { children: JSX.Element }) => { const token = authToken() return token ? children : null } -const AuthorizedRoute = ({ component: Component, admin = false, ...props }) => { +type AuthorizedRouteProps = { + component: ReactComponentLike + admin: boolean +} + +const AuthorizedRoute = ({ + component: Component, + admin = false, + ...props +}: AuthorizedRouteProps) => { const token = authToken() const isAdmin = useIsAdmin(admin) - let unauthorizedRedirect = null + let unauthorizedRedirect: null | ReactChild = null if (!token) { unauthorizedRedirect = } - let adminRedirect = null + let adminRedirect: null | ReactChild = null if (token && admin) { if (isAdmin === false) { adminRedirect = diff --git a/ui/src/helpers/LazyLoad.js b/ui/src/helpers/LazyLoad.ts similarity index 65% rename from ui/src/helpers/LazyLoad.js rename to ui/src/helpers/LazyLoad.ts index 6892bf2..f70410d 100644 --- a/ui/src/helpers/LazyLoad.js +++ b/ui/src/helpers/LazyLoad.ts @@ -1,4 +1,6 @@ class LazyLoad { + observer: null | IntersectionObserver + constructor() { this.observe = this.observe.bind(this) this.loadImages = this.loadImages.bind(this) @@ -6,22 +8,22 @@ class LazyLoad { this.observer = null } - observe(images) { + observe(images: Element[]) { if (!this.observer) { this.observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting || entry.intersectionRatio > 0) { const element = entry.target this.setSrcAttribute(element) - this.observer.unobserve(element) + this.observer?.unobserve(element) } }) }) } - Array.from(images).forEach(image => this.observer.observe(image)) + Array.from(images).forEach(image => this.observer?.observe(image)) } - loadImages(elements) { + loadImages(elements: Element[]) { const images = Array.from(elements) if (images.length) { if ('IntersectionObserver' in window) { @@ -36,11 +38,18 @@ class LazyLoad { this.observer && this.observer.disconnect() } - setSrcAttribute(element) { + setSrcAttribute(element: Element) { if (element.hasAttribute('data-src')) { const src = element.getAttribute('data-src') - element.removeAttribute('data-src') - element.setAttribute('src', src) + if (src) { + element.removeAttribute('data-src') + element.setAttribute('src', src) + } else { + console.warn( + 'WARN: expected element to have `data-src` property', + element + ) + } } } } diff --git a/ui/src/helpers/authentication.js b/ui/src/helpers/authentication.ts similarity index 75% rename from ui/src/helpers/authentication.js rename to ui/src/helpers/authentication.ts index 3a60643..abe3655 100644 --- a/ui/src/helpers/authentication.js +++ b/ui/src/helpers/authentication.ts @@ -1,4 +1,4 @@ -export function saveTokenCookie(token) { +export function saveTokenCookie(token: string) { const maxAge = 14 * 24 * 60 * 60 document.cookie = `auth-token=${token} ;max-age=${maxAge} ;path=/ ;sameSite=Lax` @@ -13,11 +13,11 @@ export function authToken() { return match && match[1] } -export function saveSharePassword(shareToken, password) { +export function saveSharePassword(shareToken: string, password: string) { document.cookie = `share-token-pw-${shareToken}=${password} ;path=/ ;sameSite=Lax` } -export function getSharePassword(shareToken) { +export function getSharePassword(shareToken: string) { const match = document.cookie.match( `share-token-pw-${shareToken}=([\\d\\w]+)` ) diff --git a/ui/src/helpers/utils.js b/ui/src/helpers/utils.js deleted file mode 100644 index b36de48..0000000 --- a/ui/src/helpers/utils.js +++ /dev/null @@ -1,28 +0,0 @@ -export function debounce(func, wait, triggerRising) { - let timeout = null - - const debounced = (...args) => { - if (timeout) { - clearTimeout(timeout) - timeout = null - } else if (triggerRising) { - func(...args) - } - - timeout = setTimeout(() => { - timeout = null - func(...args) - }, wait) - } - - debounced.cancel = () => { - clearTimeout(timeout) - timeout = null - } - - return debounced -} - -export function isNil(value) { - return value === undefined || value === null -} diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts new file mode 100644 index 0000000..e543c1b --- /dev/null +++ b/ui/src/helpers/utils.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface DebouncedFn any> { + (...args: Parameters): void + cancel(): void +} + +export function debounce any>( + func: T, + wait: number, + triggerRising?: boolean +): DebouncedFn { + let timeout: number | undefined = undefined + + const debounced = (...args: Parameters) => { + if (timeout) { + clearTimeout(timeout) + timeout = undefined + } else if (triggerRising) { + func(...args) + } + + timeout = window.setTimeout(() => { + timeout = undefined + func(...args) + }, wait) + } + + debounced.cancel = () => { + clearTimeout(timeout) + timeout = undefined + } + + return debounced +} + +export function isNil(value: any) { + return value === undefined || value === null +} diff --git a/ui/src/hooks/useScrollPagination.js b/ui/src/hooks/useScrollPagination.ts similarity index 65% rename from ui/src/hooks/useScrollPagination.js rename to ui/src/hooks/useScrollPagination.ts index 165bc87..2e7f716 100644 --- a/ui/src/hooks/useScrollPagination.js +++ b/ui/src/hooks/useScrollPagination.ts @@ -1,26 +1,44 @@ import { useCallback, useEffect, useRef, useState } from 'react' -const useScrollPagination = ({ loading, fetchMore, data, getItems }) => { - const observer = useRef(null) - const observerElem = useRef(null) +interface ScrollPaginationArgs { + loading: boolean + data: D + fetchMore(args: { variables: { offset: number } }): Promise<{ data: D }> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getItems(data: D): any[] +} + +type ScrollPaginationResult = { + finished: boolean + containerElem(node: null | Element): void +} + +function useScrollPagination({ + loading, + fetchMore, + data, + getItems, +}: ScrollPaginationArgs): ScrollPaginationResult { + const observer = useRef(null) + const observerElem = useRef(null) const [finished, setFinished] = useState(false) const reconfigureIntersectionObserver = () => { - var options = { + const options = { root: null, rootMargin: '-100% 0px 0px 0px', threshold: 0, } // delete old observer - if (observer.current) observer.current.disconnect() + observer.current?.disconnect() if (finished) return // configure new observer observer.current = new IntersectionObserver(entities => { if (entities.find(x => x.isIntersecting == false)) { - let itemCount = getItems(data).length + const itemCount = getItems(data).length fetchMore({ variables: { offset: itemCount, @@ -40,7 +58,7 @@ const useScrollPagination = ({ loading, fetchMore, data, getItems }) => { } } - const containerElem = useCallback(node => { + const containerElem = useCallback((node: null | Element): void => { observerElem.current = node // cleanup @@ -55,7 +73,7 @@ const useScrollPagination = ({ loading, fetchMore, data, getItems }) => { // only observe when not loading useEffect(() => { - if (observer.current != null) { + if (observer.current && observerElem.current) { if (loading) { observer.current.unobserve(observerElem.current) } else { diff --git a/ui/src/hooks/useURLParameters.js b/ui/src/hooks/useURLParameters.ts similarity index 79% rename from ui/src/hooks/useURLParameters.js rename to ui/src/hooks/useURLParameters.ts index 6ad3829..79c0051 100644 --- a/ui/src/hooks/useURLParameters.js +++ b/ui/src/hooks/useURLParameters.ts @@ -6,7 +6,7 @@ function useURLParameters() { const url = new URL(urlString) const params = new URLSearchParams(url.search) - const getParam = (key, defaultValue = null) => { + const getParam = (key: string, defaultValue = null) => { return params.has(key) ? params.get(key) : defaultValue } @@ -15,12 +15,12 @@ function useURLParameters() { setUrlString(document.location.href) } - const setParam = (key, value) => { + const setParam = (key: string, value: string) => { params.set(key, value) updateParams() } - const setParams = pairs => { + const setParams = (pairs: { key: string; value: string }[]) => { for (const pair of pairs) { params.set(pair.key, pair.value) } diff --git a/ui/src/localization.js b/ui/src/localization.ts similarity index 89% rename from ui/src/localization.js rename to ui/src/localization.ts index 3890d3a..882ad83 100644 --- a/ui/src/localization.js +++ b/ui/src/localization.ts @@ -1,7 +1,7 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' -export default function setupLocalization() { +export default function setupLocalization(): void { i18n.use(initReactI18next).init({ resources: { en: { diff --git a/ui/tsconfig.json b/ui/tsconfig.json index b3854d6..fd4fb47 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -4,9 +4,12 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - // "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - // "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, + "lib": [ + "es2015", + "dom" + ] /* Specify library files to be included in the compilation. */, "allowJs": true /* Allow javascript files to be compiled. */, // "checkJs": true, /* Report errors in .js files. */ "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */, @@ -48,7 +51,9 @@ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ + "typeRoots": [ + "./src/@types/" + ] /* List of folders to include type definitions from. */, // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, @@ -68,5 +73,6 @@ /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ - } + }, + "exclude": ["node_modules/", "dist/"] } From f4e65eb58e8981b621bbdd27b289e27ab487abcf Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 12 Apr 2021 22:42:23 +0200 Subject: [PATCH 19/28] Further work on typescript migration --- ui/package-lock.json | 22 +++++++- ui/package.json | 12 +++-- .../{AlbumBox.js => AlbumBox.tsx} | 31 ++++++----- .../components/messages/SubscriptionsHook.ts | 4 +- .../{PhotoGallery.js => PhotoGallery.tsx} | 41 ++++++++------- .../{ProtectedMedia.js => ProtectedMedia.tsx} | 52 +++++++++++++------ ui/src/components/routes/AuthorizedRoute.tsx | 6 +-- ui/src/components/routes/Routes.tsx | 14 ++--- ui/src/components/sidebar/MediaSidebar.js | 2 +- .../sidebar/{Sidebar.js => Sidebar.tsx} | 29 +++++++++-- ui/src/localization.ts | 4 +- ui/tsconfig.json | 3 +- 12 files changed, 146 insertions(+), 74 deletions(-) rename ui/src/components/albumGallery/{AlbumBox.js => AlbumBox.tsx} (70%) rename ui/src/components/photoGallery/{PhotoGallery.js => PhotoGallery.tsx} (78%) rename ui/src/components/photoGallery/{ProtectedMedia.js => ProtectedMedia.tsx} (54%) rename ui/src/components/sidebar/{Sidebar.js => Sidebar.tsx} (70%) diff --git a/ui/package-lock.json b/ui/package-lock.json index 2298621..d15438c 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -68,7 +68,8 @@ "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^10.5.4", - "prettier": "^2.2.1" + "prettier": "^2.2.1", + "tsc-files": "^1.1.2" } }, "node_modules/@apollo/client": { @@ -13373,6 +13374,18 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, + "node_modules/tsc-files": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tsc-files/-/tsc-files-1.1.2.tgz", + "integrity": "sha512-biLtl4npoohZ9MBnTFw4NttqYM60RscjzjWxT538UCS8iXaGRZMi+AXj+vEEpDdcjIS2Kx0Acj++1gor5dbbBw==", + "dev": true, + "bin": { + "tsc-files": "lib/index.js" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, "node_modules/tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", @@ -24801,6 +24814,13 @@ } } }, + "tsc-files": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tsc-files/-/tsc-files-1.1.2.tgz", + "integrity": "sha512-biLtl4npoohZ9MBnTFw4NttqYM60RscjzjWxT538UCS8iXaGRZMi+AXj+vEEpDdcjIS2Kx0Acj++1gor5dbbBw==", + "dev": true, + "requires": {} + }, "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", diff --git a/ui/package.json b/ui/package.json index 04be30a..f136346 100644 --- a/ui/package.json +++ b/ui/package.json @@ -57,7 +57,9 @@ "start": "node --experimental-modules build.mjs watch", "build": "NODE_ENV=production node --experimental-modules build.mjs", "test": "npm run lint && npm run jest", - "lint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js", + "lint": "npm run lint:types & npm run lint:eslint", + "lint:eslint": "eslint ./src --max-warnings 0 --cache --config .eslintrc.js", + "lint:types": "tsc --noemit", "jest": "jest", "genSchemaTypes": "npx apollo client:codegen --target=typescript", "prepare": "(cd .. && npx husky install)" @@ -77,7 +79,8 @@ "husky": "^6.0.0", "jest": "^26.6.3", "lint-staged": "^10.5.4", - "prettier": "^2.2.1" + "prettier": "^2.2.1", + "tsc-files": "^1.1.2" }, "cache": { "swDest": "service-worker.js" @@ -99,7 +102,8 @@ } }, "lint-staged": { - "*.{js,json,css,md,graphql}": "prettier --write", - "*.js": "eslint --cache --fix --max-warnings 0" + "*.{ts,tsx,js,json,css,md,graphql}": "prettier --write", + "*.{js,ts,tsx}": "eslint --cache --fix --max-warnings 0", + "*.{ts,tsx}": "tsc-files --noEmit" } } diff --git a/ui/src/components/albumGallery/AlbumBox.js b/ui/src/components/albumGallery/AlbumBox.tsx similarity index 70% rename from ui/src/components/albumGallery/AlbumBox.js rename to ui/src/components/albumGallery/AlbumBox.tsx index 1743c23..0b6364d 100644 --- a/ui/src/components/albumGallery/AlbumBox.js +++ b/ui/src/components/albumGallery/AlbumBox.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import PropTypes from 'prop-types' import styled from 'styled-components' import { Link } from 'react-router-dom' import { ProtectedImage } from '../photoGallery/ProtectedMedia' @@ -28,7 +27,7 @@ const Image = styled(ProtectedImage)` object-position: center; ` -const Placeholder = styled.div` +const Placeholder = styled.div<{ overlap?: boolean; loaded?: boolean }>` width: 220px; height: 220px; border-radius: 4%; @@ -47,14 +46,18 @@ const Placeholder = styled.div` `} ` -const AlbumBoxImage = ({ src, ...props }) => { +interface AlbumBoxImageProps { + src?: string +} + +const AlbumBoxImage = ({ src, ...props }: AlbumBoxImageProps) => { const [loaded, setLoaded] = useState(false) if (src) { return ( - setLoaded(loaded)} src={src} /> - + setLoaded(true)} src={src} /> + ) } @@ -62,11 +65,16 @@ const AlbumBoxImage = ({ src, ...props }) => { return } -AlbumBoxImage.propTypes = { - src: PropTypes.string, +type AlbumBoxProps = { + album?: { + id: string + title: string + thumbnail?: { thumbnail?: { url: string } } + } + customLink?: string } -export const AlbumBox = ({ album, customLink, ...props }) => { +export const AlbumBox = ({ album, customLink, ...props }: AlbumBoxProps) => { if (!album) { return ( @@ -75,7 +83,7 @@ export const AlbumBox = ({ album, customLink, ...props }) => { ) } - let thumbnail = album.thumbnail?.thumbnail?.url + const thumbnail = album.thumbnail?.thumbnail?.url return ( @@ -84,8 +92,3 @@ export const AlbumBox = ({ album, customLink, ...props }) => { ) } - -AlbumBox.propTypes = { - album: PropTypes.object, - customLink: PropTypes.string, -} diff --git a/ui/src/components/messages/SubscriptionsHook.ts b/ui/src/components/messages/SubscriptionsHook.ts index de1b287..18cefac 100644 --- a/ui/src/components/messages/SubscriptionsHook.ts +++ b/ui/src/components/messages/SubscriptionsHook.ts @@ -5,7 +5,7 @@ import { useSubscription, gql } from '@apollo/client' import { authToken } from '../../helpers/authentication' import { NotificationType } from '../../../__generated__/globalTypes' -const notificationSubscription = gql` +const NOTIFICATION_SUBSCRIPTION = gql` subscription notificationSubscription { notification { key @@ -49,7 +49,7 @@ const SubscriptionsHook = ({ } const { data, error } = useSubscription( - notificationSubscription + NOTIFICATION_SUBSCRIPTION ) useEffect(() => { diff --git a/ui/src/components/photoGallery/PhotoGallery.js b/ui/src/components/photoGallery/PhotoGallery.tsx similarity index 78% rename from ui/src/components/photoGallery/PhotoGallery.js rename to ui/src/components/photoGallery/PhotoGallery.tsx index f3a0c66..41d003a 100644 --- a/ui/src/components/photoGallery/PhotoGallery.js +++ b/ui/src/components/photoGallery/PhotoGallery.tsx @@ -3,8 +3,7 @@ import styled from 'styled-components' import { Loader } from 'semantic-ui-react' import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail' import PresentView from './presentView/PresentView' -import PropTypes from 'prop-types' -import { SidebarContext } from '../sidebar/Sidebar' +import { SidebarContext, UpdateSidebarFn } from '../sidebar/Sidebar' import MediaSidebar from '../sidebar/MediaSidebar' import { useTranslation } from 'react-i18next' @@ -31,6 +30,24 @@ const ClearWrap = styled.div` clear: both; ` +type PhotoGalleryProps = { + loading: boolean + media: { + id: string + title: string + thumbnail?: { + url: string + } + }[] + activeIndex: number + presenting: boolean + onSelectImage(index: number): void + setPresenting(callback: (presenting: boolean) => void): void + nextImage(): void + previousImage(): void + onFavorite(): void +} + const PhotoGallery = ({ activeIndex = -1, media, @@ -41,17 +58,15 @@ const PhotoGallery = ({ nextImage, previousImage, onFavorite, -}) => { +}: PhotoGalleryProps) => { const { t } = useTranslation() const { updateSidebar } = useContext(SidebarContext) - const activeImage = media && activeIndex != -1 && media[activeIndex] + const activeImage = (media && activeIndex != -1 && media[activeIndex]) || {} - const getPhotoElements = updateSidebar => { + const getPhotoElements = (updateSidebar: UpdateSidebarFn) => { let photoElements = [] if (media) { - media.filter(media => media.thumbnail) - photoElements = media.map((photo, index) => { const active = activeIndex == index @@ -98,16 +113,4 @@ const PhotoGallery = ({ ) } -PhotoGallery.propTypes = { - loading: PropTypes.bool, - media: PropTypes.array, - activeIndex: PropTypes.number, - presenting: PropTypes.bool, - onSelectImage: PropTypes.func, - setPresenting: PropTypes.func, - nextImage: PropTypes.func, - previousImage: PropTypes.func, - onFavorite: PropTypes.func, -} - export default PhotoGallery diff --git a/ui/src/components/photoGallery/ProtectedMedia.js b/ui/src/components/photoGallery/ProtectedMedia.tsx similarity index 54% rename from ui/src/components/photoGallery/ProtectedMedia.js rename to ui/src/components/photoGallery/ProtectedMedia.tsx index a855129..0df0091 100644 --- a/ui/src/components/photoGallery/ProtectedMedia.js +++ b/ui/src/components/photoGallery/ProtectedMedia.tsx @@ -1,11 +1,11 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, { DetailedHTMLProps, ImgHTMLAttributes } from 'react' const isNativeLazyLoadSupported = 'loading' in HTMLImageElement.prototype -const placeholder = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' +const placeholder = + 'data:image/gif;base64,R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' -const getProtectedUrl = url => { - if (url == null) return null +const getProtectedUrl = (url?: string) => { + if (url == undefined) return undefined const imgUrl = new URL(url, location.origin) @@ -18,25 +18,40 @@ const getProtectedUrl = url => { return imgUrl.href } +export interface ProtectedImageProps + extends DetailedHTMLProps< + ImgHTMLAttributes, + HTMLImageElement + > { + lazyLoading?: boolean +} + /** * An image that needs authorization to load * Set lazyLoading to true if you want the image to be loaded once it enters the viewport * Native lazy load via HTMLImageElement.loading attribute will be preferred if it is supported by the browser, * otherwise IntersectionObserver will be used. */ -export const ProtectedImage = ({ src, lazyLoading, ...props }) => { +export const ProtectedImage = ({ + src, + lazyLoading, + ...props +}: ProtectedImageProps) => { + const lazyLoadProps: { 'data-src'?: string; loading?: 'lazy' | 'eager' } = {} + if (!isNativeLazyLoadSupported && lazyLoading) { - props['data-src'] = getProtectedUrl(src) + lazyLoadProps['data-src'] = getProtectedUrl(src) } if (isNativeLazyLoadSupported && lazyLoading) { - props.loading = 'lazy' + lazyLoadProps.loading = 'lazy' } return ( { ) } -ProtectedImage.propTypes = { - src: PropTypes.string, - lazyLoading: PropTypes.bool, +export interface ProtectedVideoProps_Media { + id: string + thumbnail: null | { + url: string + } + videoWeb: { + url: string + } } -export const ProtectedVideo = ({ media, ...props }) => ( +export interface ProtectedVideoProps { + media: ProtectedVideoProps_Media +} + +export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => ( ) - -ProtectedVideo.propTypes = { - media: PropTypes.object.isRequired, -} diff --git a/ui/src/components/routes/AuthorizedRoute.tsx b/ui/src/components/routes/AuthorizedRoute.tsx index 2ba606e..6779f3d 100644 --- a/ui/src/components/routes/AuthorizedRoute.tsx +++ b/ui/src/components/routes/AuthorizedRoute.tsx @@ -1,6 +1,6 @@ import React, { ReactChild, useEffect } from 'react' import PropTypes, { ReactComponentLike } from 'prop-types' -import { Route, Redirect } from 'react-router-dom' +import { Route, Redirect, RouteProps } from 'react-router-dom' import { useLazyQuery } from '@apollo/client' import { authToken } from '../../helpers/authentication' import { ADMIN_QUERY } from '../../Layout' @@ -27,9 +27,9 @@ export const Authorized = ({ children }: { children: JSX.Element }) => { return token ? children : null } -type AuthorizedRouteProps = { +interface AuthorizedRouteProps extends Omit { component: ReactComponentLike - admin: boolean + admin?: boolean } const AuthorizedRoute = ({ diff --git a/ui/src/components/routes/Routes.tsx b/ui/src/components/routes/Routes.tsx index 0739752..6bf93eb 100644 --- a/ui/src/components/routes/Routes.tsx +++ b/ui/src/components/routes/Routes.tsx @@ -8,8 +8,8 @@ import { useTranslation } from 'react-i18next' const AuthorizedRoute = React.lazy(() => import('./AuthorizedRoute')) -const AlbumsPage = React.lazy(() => - import('../../Pages/AllAlbumsPage/AlbumsPage') +const AlbumsPage = React.lazy( + () => import('../../Pages/AllAlbumsPage/AlbumsPage') ) const AlbumPage = React.lazy(() => import('../../Pages/AlbumPage/AlbumPage')) const PhotosPage = React.lazy(() => import('../../Pages/PhotosPage/PhotosPage')) @@ -18,12 +18,12 @@ const SharePage = React.lazy(() => import('../../Pages/SharePage/SharePage')) const PeoplePage = React.lazy(() => import('../../Pages/PeoplePage/PeoplePage')) const LoginPage = React.lazy(() => import('../../Pages/LoginPage/LoginPage')) -const InitialSetupPage = React.lazy(() => - import('../../Pages/LoginPage/InitialSetupPage') +const InitialSetupPage = React.lazy( + () => import('../../Pages/LoginPage/InitialSetupPage') ) -const SettingsPage = React.lazy(() => - import('../../Pages/SettingsPage/SettingsPage') +const SettingsPage = React.lazy( + () => import('../../Pages/SettingsPage/SettingsPage') ) const Routes = () => { @@ -32,7 +32,7 @@ const Routes = () => { return ( + {t('general.loading.page', 'Loading page')} } diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index e1d2bdf..0bc82be 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -370,7 +370,7 @@ const MediaSidebar = ({ media, hidePreview }) => { return } - if (error) return error + if (error) return
{error.message}
if (loading || data == null) { return diff --git a/ui/src/components/sidebar/Sidebar.js b/ui/src/components/sidebar/Sidebar.tsx similarity index 70% rename from ui/src/components/sidebar/Sidebar.js rename to ui/src/components/sidebar/Sidebar.tsx index 508d263..aed641a 100644 --- a/ui/src/components/sidebar/Sidebar.js +++ b/ui/src/components/sidebar/Sidebar.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import styled from 'styled-components' import { Icon } from 'semantic-ui-react' -const SidebarContainer = styled.div` +const SidebarContainer = styled.div<{ highlighted: boolean }>` width: 28vw; max-width: 500px; min-width: 300px; @@ -38,15 +38,34 @@ const SidebarDismissButton = styled(Icon)` } ` -export const SidebarContext = createContext() +export type UpdateSidebarFn = (content: React.ReactNode) => void + +interface SidebarContextType { + updateSidebar: UpdateSidebarFn + content: React.ReactNode +} + +export const SidebarContext = createContext({ + updateSidebar: content => { + console.warn( + 'SidebarContext: updateSidebar was called before initialezed', + content + ) + }, + content: null, +}) SidebarContext.displayName = 'SidebarContext' -const Sidebar = ({ children }) => { - const [state, setState] = useState({ +type SidebarProps = { + children: React.ReactElement +} + +const Sidebar = ({ children }: SidebarProps) => { + const [state, setState] = useState<{ content: React.ReactNode | null }>({ content: null, }) - const update = content => { + const update = (content: React.ReactNode | null) => { setState({ content }) } diff --git a/ui/src/localization.ts b/ui/src/localization.ts index 882ad83..f770aab 100644 --- a/ui/src/localization.ts +++ b/ui/src/localization.ts @@ -1,5 +1,7 @@ import i18n from 'i18next' -import { initReactI18next } from 'react-i18next' +import { initReactI18next, TFunction } from 'react-i18next' + +export type TranslationFn = TFunction<'translation'> export default function setupLocalization(): void { i18n.use(initReactI18next).init({ diff --git a/ui/tsconfig.json b/ui/tsconfig.json index fd4fb47..76533d9 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -74,5 +74,6 @@ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, - "exclude": ["node_modules/", "dist/"] + "include": ["src/**/*"], + "exclude": ["node_modules/**/*", "dist/**/*"] } From af0794dacfb48e949e57a145d96f9d676b0a998e Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 12 Apr 2021 23:36:29 +0200 Subject: [PATCH 20/28] Rewrite photoGallery component to TS --- .../{MediaThumbnail.js => MediaThumbnail.tsx} | 68 ++++++++++--------- .../components/photoGallery/PhotoGallery.tsx | 29 +++++--- .../photoGallery/ProtectedMedia.tsx | 22 +++--- .../photoGallery/presentView/PresentMedia.js | 53 --------------- .../photoGallery/presentView/PresentMedia.tsx | 66 ++++++++++++++++++ ...verlay.js => PresentNavigationOverlay.tsx} | 33 +++++---- .../{PresentView.js => PresentView.tsx} | 29 ++++---- .../presentView/icons/{Exit.js => Exit.tsx} | 2 +- .../presentView/icons/{Next.js => Next.tsx} | 2 +- .../icons/{Previous.js => Previous.tsx} | 2 +- ui/tsconfig.json | 2 +- 11 files changed, 169 insertions(+), 139 deletions(-) rename ui/src/components/photoGallery/{MediaThumbnail.js => MediaThumbnail.tsx} (79%) delete mode 100644 ui/src/components/photoGallery/presentView/PresentMedia.js create mode 100644 ui/src/components/photoGallery/presentView/PresentMedia.tsx rename ui/src/components/photoGallery/presentView/{PresentNavigationOverlay.js => PresentNavigationOverlay.tsx} (76%) rename ui/src/components/photoGallery/presentView/{PresentView.js => PresentView.tsx} (74%) rename ui/src/components/photoGallery/presentView/icons/{Exit.js => Exit.tsx} (88%) rename ui/src/components/photoGallery/presentView/icons/{Next.js => Next.tsx} (87%) rename ui/src/components/photoGallery/presentView/icons/{Previous.js => Previous.tsx} (86%) diff --git a/ui/src/components/photoGallery/MediaThumbnail.js b/ui/src/components/photoGallery/MediaThumbnail.tsx similarity index 79% rename from ui/src/components/photoGallery/MediaThumbnail.js rename to ui/src/components/photoGallery/MediaThumbnail.tsx index a3b0fcf..3c583e7 100644 --- a/ui/src/components/photoGallery/MediaThumbnail.js +++ b/ui/src/components/photoGallery/MediaThumbnail.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useState } from 'react' import { useMutation, gql } from '@apollo/client' -import PropTypes from 'prop-types' import styled from 'styled-components' import { Icon } from 'semantic-ui-react' import { ProtectedImage } from './ProtectedMedia' +import { MediaType } from '../../../__generated__/globalTypes' const markFavoriteMutation = gql` mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) { @@ -24,7 +24,7 @@ const MediaContainer = styled.div` overflow: hidden; ` -const StyledPhoto = styled(ProtectedImage)` +const StyledPhoto = styled(ProtectedImage)<{ loaded: boolean }>` height: 200px; min-width: 100%; position: relative; @@ -34,35 +34,30 @@ const StyledPhoto = styled(ProtectedImage)` transition: opacity 300ms; ` -const LazyPhoto = photoProps => { +type LazyPhotoProps = { + src?: string +} + +const LazyPhoto = (photoProps: LazyPhotoProps) => { const [loaded, setLoaded] = useState(false) const onLoad = useCallback(e => { !e.target.dataset.src && setLoaded(true) }, []) return ( - + ) } -LazyPhoto.propTypes = { - src: PropTypes.string, -} - -const PhotoOverlay = styled.div` +const PhotoOverlay = styled.div<{ active: boolean }>` width: 100%; height: 100%; position: absolute; top: 0; left: 0; - ${props => - props.active && + ${({ active }) => + active && ` border: 4px solid rgba(65, 131, 196, 0.6); @@ -109,6 +104,24 @@ const VideoThumbnailIcon = styled(Icon)` top: calc(50% - 13px); ` +type MediaThumbnailProps = { + media: { + id: string + type: MediaType + favorite?: boolean + thumbnail: null | { + url: string + width: number + height: number + } + } + onSelectImage(index: number): void + index: number + active: boolean + setPresenting(presenting: boolean): void + onFavorite(): void +} + export const MediaThumbnail = ({ media, onSelectImage, @@ -116,16 +129,16 @@ export const MediaThumbnail = ({ active, setPresenting, onFavorite, -}) => { +}: MediaThumbnailProps) => { const [markFavorite] = useMutation(markFavoriteMutation) let heartIcon = null - if (typeof media.favorite == 'boolean') { + if (media.favorite !== undefined) { heartIcon = ( { + onClick={(event: MouseEvent) => { event.stopPropagation() const favorite = !media.favorite markFavorite({ @@ -148,7 +161,7 @@ export const MediaThumbnail = ({ } let videoIcon = null - if (media.type == 'video') { + if (media.type == MediaType.Video) { videoIcon = } @@ -163,11 +176,11 @@ export const MediaThumbnail = ({ { - onSelectImage && onSelectImage(index) + onSelectImage(index) }} >
- +
{videoIcon} @@ -192,15 +205,6 @@ export const MediaThumbnail = ({ ) } -MediaThumbnail.propTypes = { - media: PropTypes.object.isRequired, - onSelectImage: PropTypes.func, - index: PropTypes.number.isRequired, - active: PropTypes.bool.isRequired, - setPresenting: PropTypes.func.isRequired, - onFavorite: PropTypes.func, -} - export const PhotoThumbnail = styled.div` flex-grow: 1; height: 200px; diff --git a/ui/src/components/photoGallery/PhotoGallery.tsx b/ui/src/components/photoGallery/PhotoGallery.tsx index 41d003a..0cc3474 100644 --- a/ui/src/components/photoGallery/PhotoGallery.tsx +++ b/ui/src/components/photoGallery/PhotoGallery.tsx @@ -6,6 +6,7 @@ import PresentView from './presentView/PresentView' import { SidebarContext, UpdateSidebarFn } from '../sidebar/Sidebar' import MediaSidebar from '../sidebar/MediaSidebar' import { useTranslation } from 'react-i18next' +import { PresentMediaProps_Media } from './presentView/PresentMedia' const Gallery = styled.div` display: flex; @@ -30,19 +31,21 @@ const ClearWrap = styled.div` clear: both; ` +interface PhotoGalleryProps_Media extends PresentMediaProps_Media { + thumbnail: null | { + url: string + width: number + height: number + } +} + type PhotoGalleryProps = { loading: boolean - media: { - id: string - title: string - thumbnail?: { - url: string - } - }[] + media: PhotoGalleryProps_Media[] activeIndex: number presenting: boolean onSelectImage(index: number): void - setPresenting(callback: (presenting: boolean) => void): void + setPresenting(presenting: boolean): void nextImage(): void previousImage(): void onFavorite(): void @@ -62,7 +65,15 @@ const PhotoGallery = ({ const { t } = useTranslation() const { updateSidebar } = useContext(SidebarContext) - const activeImage = (media && activeIndex != -1 && media[activeIndex]) || {} + if ( + media === undefined || + activeIndex === -1 || + media[activeIndex] === undefined + ) { + return null + } + + const activeImage = media[activeIndex] const getPhotoElements = (updateSidebar: UpdateSidebarFn) => { let photoElements = [] diff --git a/ui/src/components/photoGallery/ProtectedMedia.tsx b/ui/src/components/photoGallery/ProtectedMedia.tsx index 0df0091..445579e 100644 --- a/ui/src/components/photoGallery/ProtectedMedia.tsx +++ b/ui/src/components/photoGallery/ProtectedMedia.tsx @@ -19,10 +19,12 @@ const getProtectedUrl = (url?: string) => { } export interface ProtectedImageProps - extends DetailedHTMLProps< - ImgHTMLAttributes, - HTMLImageElement + extends Omit< + DetailedHTMLProps, HTMLImageElement>, + 'src' > { + src?: string + key?: string lazyLoading?: boolean } @@ -34,6 +36,7 @@ export interface ProtectedImageProps */ export const ProtectedImage = ({ src, + key, lazyLoading, ...props }: ProtectedImageProps) => { @@ -47,16 +50,17 @@ export const ProtectedImage = ({ lazyLoadProps.loading = 'lazy' } + const imgSrc: string = + lazyLoading && !isNativeLazyLoadSupported + ? placeholder + : getProtectedUrl(src) || placeholder + return ( ) diff --git a/ui/src/components/photoGallery/presentView/PresentMedia.js b/ui/src/components/photoGallery/presentView/PresentMedia.js deleted file mode 100644 index 79d1377..0000000 --- a/ui/src/components/photoGallery/presentView/PresentMedia.js +++ /dev/null @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import styled from 'styled-components' -import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia' - -const StyledPhoto = styled(ProtectedImage)` - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - object-fit: contain; - object-position: center; -` - -const StyledVideo = styled(ProtectedVideo)` - position: absolute; - top: 0; - left: 0; - width: 100vw; - height: 100vh; -` - -const PresentMedia = ({ media, imageLoaded, ...otherProps }) => { - if (media.type == 'photo') { - return ( -
- - { - e.target.style.display = 'initial' - imageLoaded && imageLoaded() - }} - /> -
- ) - } - - if (media.type == 'video') { - return - } - - throw new Error(`Unknown media type '${media.type}'`) -} - -PresentMedia.propTypes = { - media: PropTypes.object.isRequired, - imageLoaded: PropTypes.func, -} - -export default PresentMedia diff --git a/ui/src/components/photoGallery/presentView/PresentMedia.tsx b/ui/src/components/photoGallery/presentView/PresentMedia.tsx new file mode 100644 index 0000000..228e3a7 --- /dev/null +++ b/ui/src/components/photoGallery/presentView/PresentMedia.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import styled from 'styled-components' +import { MediaType } from '../../../../__generated__/globalTypes' +import { + ProtectedImage, + ProtectedVideo, + ProtectedVideoProps_Media, +} from '../ProtectedMedia' + +const StyledPhoto = styled(ProtectedImage)` + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + object-fit: contain; + object-position: center; +` + +const StyledVideo = styled(ProtectedVideo)` + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; +` + +export interface PresentMediaProps_Media extends ProtectedVideoProps_Media { + type: MediaType + highRes?: { + url: string + } +} + +type PresentMediaProps = { + media: PresentMediaProps_Media + imageLoaded?(): void +} + +const PresentMedia = ({ + media, + imageLoaded, + ...otherProps +}: PresentMediaProps) => { + switch (media.type) { + case MediaType.Photo: + return ( +
+ + { + const elem = e.target as HTMLImageElement + elem.style.display = 'initial' + imageLoaded && imageLoaded() + }} + /> +
+ ) + case MediaType.Video: + return + } +} + +export default PresentMedia diff --git a/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.js b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx similarity index 76% rename from ui/src/components/photoGallery/presentView/PresentNavigationOverlay.js rename to ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx index 99121b5..8d99e05 100644 --- a/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.js +++ b/ui/src/components/photoGallery/presentView/PresentNavigationOverlay.tsx @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect } from 'react' -import PropTypes from 'prop-types' import styled from 'styled-components' -import { debounce } from '../../../helpers/utils' +import { debounce, DebouncedFn } from '../../../helpers/utils' import ExitIcon from './icons/Exit' import NextIcon from './icons/Next' @@ -50,7 +49,7 @@ const ExitButton = styled(OverlayButton)` top: 28px; ` -const NavigationButton = styled(OverlayButton)` +const NavigationButton = styled(OverlayButton)<{ float: 'left' | 'right' }>` height: 80%; width: 20%; top: 10%; @@ -64,14 +63,21 @@ const NavigationButton = styled(OverlayButton)` } ` +type PresentNavigationOverlayProps = { + children?: React.ReactChild + nextImage(): void + previousImage(): void + setPresenting(presenting: boolean): void +} + const PresentNavigationOverlay = ({ children, nextImage, previousImage, setPresenting, -}) => { +}: PresentNavigationOverlayProps) => { const [hide, setHide] = useState(true) - const onMouseMove = useRef(null) + const onMouseMove = useRef void>>(null) useEffect(() => { onMouseMove.current = debounce( @@ -83,33 +89,33 @@ const PresentNavigationOverlay = ({ ) return () => { - onMouseMove.current.cancel() + onMouseMove.current?.cancel() } }, []) return ( { - onMouseMove.current() + onMouseMove.current && onMouseMove.current() }} > {children} previousImage()} > nextImage()} > setPresenting(false)} > @@ -118,11 +124,4 @@ const PresentNavigationOverlay = ({ ) } -PresentNavigationOverlay.propTypes = { - children: PropTypes.element, - nextImage: PropTypes.func.isRequired, - previousImage: PropTypes.func.isRequired, - setPresenting: PropTypes.func.isRequired, -} - export default PresentNavigationOverlay diff --git a/ui/src/components/photoGallery/presentView/PresentView.js b/ui/src/components/photoGallery/presentView/PresentView.tsx similarity index 74% rename from ui/src/components/photoGallery/presentView/PresentView.js rename to ui/src/components/photoGallery/presentView/PresentView.tsx index 1650188..c8fb3b4 100644 --- a/ui/src/components/photoGallery/presentView/PresentView.js +++ b/ui/src/components/photoGallery/presentView/PresentView.tsx @@ -1,8 +1,7 @@ import React, { useEffect } from 'react' -import PropTypes from 'prop-types' import styled, { createGlobalStyle } from 'styled-components' import PresentNavigationOverlay from './PresentNavigationOverlay' -import PresentMedia from './PresentMedia' +import PresentMedia, { PresentMediaProps_Media } from './PresentMedia' const StyledContainer = styled.div` position: fixed; @@ -21,6 +20,15 @@ const PreventScroll = createGlobalStyle` } ` +type PresentViewProps = { + media: PresentMediaProps_Media + className?: string + imageLoaded?(): void + nextImage(): void + previousImage(): void + setPresenting(presenting: boolean): void +} + const PresentView = ({ className, media, @@ -28,16 +36,16 @@ const PresentView = ({ nextImage, previousImage, setPresenting, -}) => { +}: PresentViewProps) => { useEffect(() => { - const keyDownEvent = e => { + const keyDownEvent = (e: KeyboardEvent) => { if (e.key == 'ArrowRight') { - nextImage && nextImage() + nextImage() e.stopPropagation() } if (e.key == 'ArrowLeft') { - nextImage && previousImage() + previousImage() e.stopPropagation() } @@ -66,13 +74,4 @@ const PresentView = ({ ) } -PresentView.propTypes = { - media: PropTypes.object.isRequired, - imageLoaded: PropTypes.func, - className: PropTypes.string, - nextImage: PropTypes.func.isRequired, - previousImage: PropTypes.func.isRequired, - setPresenting: PropTypes.func.isRequired, -} - export default PresentView diff --git a/ui/src/components/photoGallery/presentView/icons/Exit.js b/ui/src/components/photoGallery/presentView/icons/Exit.tsx similarity index 88% rename from ui/src/components/photoGallery/presentView/icons/Exit.js rename to ui/src/components/photoGallery/presentView/icons/Exit.tsx index bf8a328..c26952f 100644 --- a/ui/src/components/photoGallery/presentView/icons/Exit.js +++ b/ui/src/components/photoGallery/presentView/icons/Exit.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -function SvgExit(props) { +function SvgExit(props: React.SVGProps) { return ( ) { return ( ) { return ( Date: Tue, 13 Apr 2021 14:16:40 +0200 Subject: [PATCH 21/28] Fix UI tests --- ui/babel.config.js | 4 +- ui/package-lock.json | 68 ++++++++++--------- ui/package.json | 8 +-- ui/src/Layout.test.js | 2 +- .../components/routes/AuthorizedRoute.test.js | 2 +- ui/tsconfig.json | 4 +- 6 files changed, 45 insertions(+), 43 deletions(-) diff --git a/ui/babel.config.js b/ui/babel.config.js index 5cdd249..0f6c8a3 100644 --- a/ui/babel.config.js +++ b/ui/babel.config.js @@ -2,14 +2,12 @@ module.exports = function (api) { const isTest = api.env('test') const isProduction = api.env('NODE_ENV') == 'production' - let presets = ['@babel/preset-typescript', '@babel/preset-react'] + let presets = ['@babel/preset-react', '@babel/preset-typescript'] let plugins = [] if (isTest) { presets.push('@babel/preset-env') - plugins.push('@babel/plugin-transform-runtime') - plugins.push('@babel/plugin-transform-modules-commonjs') } else { plugins.push(['styled-components', { pure: true }]) plugins.push('graphql-tag') diff --git a/ui/package-lock.json b/ui/package-lock.json index d15438c..c0733f6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -11,9 +11,6 @@ "dependencies": { "@apollo/client": "^3.3.14", "@babel/core": "^7.13.15", - "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/plugin-transform-runtime": "^7.13.15", - "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", "@babel/preset-typescript": "^7.13.0", "babel-eslint": "^10.1.0", @@ -54,8 +51,11 @@ "workbox-build": "^6.1.2" }, "devDependencies": { + "@babel/plugin-transform-runtime": "^7.13.15", + "@babel/preset-env": "^7.13.15", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", + "@types/jest": "^26.0.22", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", "@types/react-helmet": "^6.1.1", @@ -163,20 +163,6 @@ "@babel/highlight": "^7.12.13" } }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -1133,6 +1119,7 @@ "version": "7.13.15", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz", "integrity": "sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==", + "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.13.12", "@babel/helper-plugin-utils": "^7.13.0", @@ -1149,6 +1136,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -2793,9 +2781,9 @@ } }, "node_modules/@types/jest": { - "version": "26.0.15", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz", - "integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==", + "version": "26.0.22", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz", + "integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==", "dev": true, "dependencies": { "jest-diff": "^26.0.0", @@ -9445,6 +9433,20 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -14245,14 +14247,6 @@ "@babel/highlight": "^7.12.13" } }, - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "requires": { - "minimist": "^1.2.5" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -15041,6 +15035,7 @@ "version": "7.13.15", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz", "integrity": "sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==", + "dev": true, "requires": { "@babel/helper-module-imports": "^7.13.12", "@babel/helper-plugin-utils": "^7.13.0", @@ -15053,7 +15048,8 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -16419,9 +16415,9 @@ } }, "@types/jest": { - "version": "26.0.15", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz", - "integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==", + "version": "26.0.22", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz", + "integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==", "dev": true, "requires": { "jest-diff": "^26.0.0", @@ -21651,6 +21647,14 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "requires": { + "minimist": "^1.2.5" + } + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", diff --git a/ui/package.json b/ui/package.json index f136346..046ec5b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,9 +11,6 @@ "dependencies": { "@apollo/client": "^3.3.14", "@babel/core": "^7.13.15", - "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/plugin-transform-runtime": "^7.13.15", - "@babel/preset-env": "^7.13.15", "@babel/preset-react": "^7.13.13", "@babel/preset-typescript": "^7.13.0", "babel-eslint": "^10.1.0", @@ -65,8 +62,11 @@ "prepare": "(cd .. && npx husky install)" }, "devDependencies": { + "@babel/plugin-transform-runtime": "^7.13.15", + "@babel/preset-env": "^7.13.15", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", + "@types/jest": "^26.0.22", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", "@types/react-helmet": "^6.1.1", @@ -97,7 +97,7 @@ "^.+\\.css$" ], "transform": { - "^.+\\.js$": "babel-jest", + "^.+\\.(js|ts|tsx)$": "babel-jest", "^.+\\.svg$": "/testing/transform-svg.js" } }, diff --git a/ui/src/Layout.test.js b/ui/src/Layout.test.js index b40a3aa..916fcd6 100644 --- a/ui/src/Layout.test.js +++ b/ui/src/Layout.test.js @@ -11,7 +11,7 @@ import * as authentication from './helpers/authentication' require('./localization').default() -jest.mock('./helpers/authentication.js') +jest.mock('./helpers/authentication.ts') test('Layout component', async () => { render( diff --git a/ui/src/components/routes/AuthorizedRoute.test.js b/ui/src/components/routes/AuthorizedRoute.test.js index a410a80..50fbf7c 100644 --- a/ui/src/components/routes/AuthorizedRoute.test.js +++ b/ui/src/components/routes/AuthorizedRoute.test.js @@ -7,7 +7,7 @@ import { MemoryRouter, Route } from 'react-router-dom' import * as authentication from '../../helpers/authentication' -jest.mock('../../helpers/authentication.js') +jest.mock('../../helpers/authentication.ts') describe('AuthorizedRoute component', () => { const AuthorizedComponent = () =>
authorized content
diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 78afcc4..547c11a 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -3,7 +3,7 @@ /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ + "incremental": true /* Enable incremental compilation */, "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "lib": [ @@ -23,7 +23,7 @@ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ - "noEmit": true /* Do not emit outputs. */, + // "noEmit": true /* Do not emit outputs. */, // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ From 34a411be9d884eb917641cc81251538a7c30a55e Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Tue, 13 Apr 2021 15:23:06 +0200 Subject: [PATCH 22/28] Rewrite SharePage to typescript --- .../{AlbumSharePage.js => AlbumSharePage.tsx} | 19 +++--- ui/src/Pages/SharePage/MediaSharePage.js | 58 ------------------ ui/src/Pages/SharePage/MediaSharePage.tsx | 61 +++++++++++++++++++ ui/src/Pages/SharePage/SharePage.test.js | 2 +- .../SharePage/{SharePage.js => SharePage.tsx} | 54 ++++++++-------- .../{AlbumGallery.js => AlbumGallery.tsx} | 48 ++++++++------- .../photoGallery/MediaThumbnail.tsx | 2 +- .../components/photoGallery/PhotoGallery.tsx | 2 +- .../photoGallery/ProtectedMedia.tsx | 31 ++++++---- .../photoGallery/presentView/PresentMedia.tsx | 2 +- ui/src/helpers/utils.ts | 2 +- ui/tsconfig.json | 2 +- 12 files changed, 151 insertions(+), 132 deletions(-) rename ui/src/Pages/SharePage/{AlbumSharePage.js => AlbumSharePage.tsx} (87%) delete mode 100644 ui/src/Pages/SharePage/MediaSharePage.js create mode 100644 ui/src/Pages/SharePage/MediaSharePage.tsx rename ui/src/Pages/SharePage/{SharePage.js => SharePage.tsx} (83%) rename ui/src/components/albumGallery/{AlbumGallery.js => AlbumGallery.tsx} (76%) diff --git a/ui/src/Pages/SharePage/AlbumSharePage.js b/ui/src/Pages/SharePage/AlbumSharePage.tsx similarity index 87% rename from ui/src/Pages/SharePage/AlbumSharePage.js rename to ui/src/Pages/SharePage/AlbumSharePage.tsx index 80045b7..ced0951 100644 --- a/ui/src/Pages/SharePage/AlbumSharePage.js +++ b/ui/src/Pages/SharePage/AlbumSharePage.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types' import React from 'react' import Layout from '../../Layout' import AlbumGallery from '../../components/albumGallery/AlbumGallery' @@ -73,7 +72,13 @@ const AlbumSharePageWrapper = styled.div` height: 100%; ` -const AlbumSharePage = ({ albumID, token, password }) => { +type AlbumSharePageProps = { + albumID: string + token: string + password: string | null +} + +const AlbumSharePage = ({ albumID, token, password }: AlbumSharePageProps) => { const { t } = useTranslation() const { data, loading, error } = useQuery(SHARE_ALBUM_QUERY, { variables: { @@ -86,11 +91,11 @@ const AlbumSharePage = ({ albumID, token, password }) => { }) if (error) { - return error.message + return
{error.message}
} if (loading) { - return t('general.loading.default', 'Loading...') + return
{t('general.loading.default', 'Loading...')}
} const album = data.album @@ -111,10 +116,4 @@ const AlbumSharePage = ({ albumID, token, password }) => { ) } -AlbumSharePage.propTypes = { - albumID: PropTypes.string.isRequired, - token: PropTypes.string.isRequired, - password: PropTypes.string, -} - export default AlbumSharePage diff --git a/ui/src/Pages/SharePage/MediaSharePage.js b/ui/src/Pages/SharePage/MediaSharePage.js deleted file mode 100644 index 92da5f3..0000000 --- a/ui/src/Pages/SharePage/MediaSharePage.js +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useContext, useEffect } from 'react' -import PropTypes from 'prop-types' -import styled from 'styled-components' -import Layout from '../../Layout' -import { - ProtectedImage, - ProtectedVideo, -} from '../../components/photoGallery/ProtectedMedia' -import { SidebarContext } from '../../components/sidebar/Sidebar' -import MediaSidebar from '../../components/sidebar/MediaSidebar' - -const DisplayPhoto = styled(ProtectedImage)` - width: 100%; - max-height: calc(80vh); - object-fit: contain; -` - -const DisplayVideo = styled(ProtectedVideo)` - width: 100%; - max-height: calc(80vh); -` - -const MediaView = ({ media }) => { - const { updateSidebar } = useContext(SidebarContext) - - useEffect(() => { - updateSidebar() - }, [media]) - - if (media.type == 'photo') { - return - } - - if (media.type == 'video') { - return - } - - throw new Error(`Unsupported media type: ${media.type}`) -} - -MediaView.propTypes = { - media: PropTypes.object.isRequired, -} - -const MediaSharePage = ({ media }) => ( - -
-

{media.title}

- -
-
-) - -MediaSharePage.propTypes = { - media: PropTypes.object.isRequired, -} - -export default MediaSharePage diff --git a/ui/src/Pages/SharePage/MediaSharePage.tsx b/ui/src/Pages/SharePage/MediaSharePage.tsx new file mode 100644 index 0000000..ce9785a --- /dev/null +++ b/ui/src/Pages/SharePage/MediaSharePage.tsx @@ -0,0 +1,61 @@ +import React, { useContext, useEffect } from 'react' +import styled from 'styled-components' +import Layout from '../../Layout' +import { + ProtectedImage, + ProtectedVideo, +} from '../../components/photoGallery/ProtectedMedia' +import { SidebarContext } from '../../components/sidebar/Sidebar' +import MediaSidebar from '../../components/sidebar/MediaSidebar' +import { useTranslation } from 'react-i18next' +import { SharePageToken_shareToken_media } from './__generated__/SharePageToken' +import { MediaType } from '../../../__generated__/globalTypes' + +const DisplayPhoto = styled(ProtectedImage)` + width: 100%; + max-height: calc(80vh); + object-fit: contain; +` + +const DisplayVideo = styled(ProtectedVideo)` + width: 100%; + max-height: calc(80vh); +` + +type MediaViewProps = { + media: SharePageToken_shareToken_media +} + +const MediaView = ({ media }: MediaViewProps) => { + const { updateSidebar } = useContext(SidebarContext) + + useEffect(() => { + updateSidebar() + }, [media]) + + switch (media.type) { + case MediaType.Photo: + return + case MediaType.Video: + return + } +} + +type MediaSharePageType = { + media: SharePageToken_shareToken_media +} + +const MediaSharePage = ({ media }: MediaSharePageType) => { + const { t } = useTranslation() + + return ( + +
+

{media.title}

+ +
+
+ ) +} + +export default MediaSharePage diff --git a/ui/src/Pages/SharePage/SharePage.test.js b/ui/src/Pages/SharePage/SharePage.test.js index fac0019..29f824f 100644 --- a/ui/src/Pages/SharePage/SharePage.test.js +++ b/ui/src/Pages/SharePage/SharePage.test.js @@ -82,7 +82,7 @@ describe('load correct share page, based on graphql query', () => { media: { id: '1', title: 'shared_image.jpg', - type: 'photo', + type: 'Photo', highRes: { url: 'https://example.com/shared_image.jpg', }, diff --git a/ui/src/Pages/SharePage/SharePage.js b/ui/src/Pages/SharePage/SharePage.tsx similarity index 83% rename from ui/src/Pages/SharePage/SharePage.js rename to ui/src/Pages/SharePage/SharePage.tsx index 7620065..076d532 100644 --- a/ui/src/Pages/SharePage/SharePage.js +++ b/ui/src/Pages/SharePage/SharePage.tsx @@ -1,8 +1,7 @@ import PropTypes from 'prop-types' import React, { useState } from 'react' import { useQuery, gql } from '@apollo/client' -import { Route, Switch } from 'react-router-dom' -import RouterProps from 'react-router-prop-types' +import { match as MatchType, Route, Switch } from 'react-router-dom' import { Form, Header, Icon, Input, Message } from 'semantic-ui-react' import styled from 'styled-components' import { @@ -71,7 +70,7 @@ export const VALIDATE_TOKEN_PASSWORD_QUERY = gql` } ` -const AuthorizedTokenRoute = ({ match }) => { +const AuthorizedTokenRoute = ({ match }: MatchProps) => { const { t } = useTranslation() const token = match.params.token @@ -84,11 +83,11 @@ const AuthorizedTokenRoute = ({ match }) => { }, }) - if (error) return error.message - if (loading) return 'Loading...' + if (error) return
{error.message}
+ if (loading) return
{t('general.loading.default', 'Loading...')}
if (data.shareToken.album) { - const SharedSubAlbumPage = ({ match }) => { + const SharedSubAlbumPage = ({ match }: MatchProps) => { return ( { +}: ProtectedTokenEnterPasswordProps) => { const { t } = useTranslation() const [passwordValue, setPasswordValue] = useState('') @@ -178,7 +182,9 @@ const ProtectedTokenEnterPassword = ({ event.key == 'Enter' && onSubmit()} + onKeyUp={(event: KeyboardEvent) => + event.key == 'Enter' && onSubmit() + } onChange={e => setPasswordValue(e.target.value)} placeholder={t('login_page.field.password', 'Password')} type="password" @@ -191,12 +197,19 @@ const ProtectedTokenEnterPassword = ({ ) } -ProtectedTokenEnterPassword.propTypes = { - refetchWithPassword: PropTypes.func.isRequired, - loading: PropTypes.bool, +interface TokenRouteMatch { + token: string } -const TokenRoute = ({ match }) => { +interface SubalbumRouteMatch extends TokenRouteMatch { + subAlbum: string +} + +interface MatchProps { + match: MatchType +} + +const TokenRoute = ({ match }: MatchProps) => { const { t } = useTranslation() const token = match.params.token @@ -227,13 +240,12 @@ const TokenRoute = ({ match }) => { ) } - return error.message + return
{error.message}
} if (data && data.shareTokenValidatePassword == false) { return ( { saveSharePassword(token, password) refetch({ token, password }) @@ -243,22 +255,18 @@ const TokenRoute = ({ match }) => { ) } - if (loading) return t('general.loading.default', 'Loading...') + if (loading) return
{t('general.loading.default', 'Loading...')}
return } -TokenRoute.propTypes = { - match: PropTypes.object.isRequired, -} - -const SharePage = ({ match }) => { +const SharePage = ({ match }: { match: MatchType }) => { const { t } = useTranslation() return ( - {({ match }) => { + {({ match }: { match: MatchType }) => { return }} @@ -267,8 +275,4 @@ const SharePage = ({ match }) => { ) } -SharePage.propTypes = { - ...RouterProps, -} - export default SharePage diff --git a/ui/src/components/albumGallery/AlbumGallery.js b/ui/src/components/albumGallery/AlbumGallery.tsx similarity index 76% rename from ui/src/components/albumGallery/AlbumGallery.js rename to ui/src/components/albumGallery/AlbumGallery.tsx index e4d7b89..4c9fd92 100644 --- a/ui/src/components/albumGallery/AlbumGallery.js +++ b/ui/src/components/albumGallery/AlbumGallery.tsx @@ -1,9 +1,21 @@ import React, { useState, useEffect } from 'react' -import PropTypes from 'prop-types' import AlbumTitle from '../AlbumTitle' import PhotoGallery from '../photoGallery/PhotoGallery' import AlbumBoxes from './AlbumBoxes' import AlbumFilter from '../AlbumFilter' +import { albumQuery_album } from '../../Pages/AlbumPage/__generated__/albumQuery' + +type AlbumGalleryProps = { + album: albumQuery_album + loading?: boolean + customAlbumLink?(albumID: string): string + showFilter?: boolean + setOnlyFavorites?(favorites: boolean): void + setOrdering?(ordering: { orderBy: string }): void + ordering?: { orderBy: string } + onlyFavorites?: boolean + onFavorite?(): void +} const AlbumGallery = React.forwardRef( ( @@ -17,18 +29,23 @@ const AlbumGallery = React.forwardRef( ordering, onlyFavorites = false, onFavorite, - }, - ref + }: AlbumGalleryProps, + ref: React.ForwardedRef ) => { - const [imageState, setImageState] = useState({ + type ImageStateType = { + activeImage: number + presenting: boolean + } + + const [imageState, setImageState] = useState({ activeImage: -1, presenting: false, }) - const setPresenting = presenting => + const setPresenting = (presenting: boolean) => setImageState(state => ({ ...state, presenting })) - const setPresentingWithHistory = presenting => { + const setPresentingWithHistory = (presenting: boolean) => { setPresenting(presenting) if (presenting) { history.pushState({ imageState }, '') @@ -37,12 +54,12 @@ const AlbumGallery = React.forwardRef( } } - const updateHistory = imageState => { + const updateHistory = (imageState: ImageStateType) => { history.replaceState({ imageState }, '') return imageState } - const setActiveImage = activeImage => { + const setActiveImage = (activeImage: number) => { setImageState(state => updateHistory({ ...state, activeImage })) } @@ -59,9 +76,10 @@ const AlbumGallery = React.forwardRef( } useEffect(() => { - const updateImageState = event => { + const updateImageState = (event: PopStateEvent) => { setImageState(event.state.imageState) } + window.addEventListener('popstate', updateImageState) return () => { @@ -129,16 +147,4 @@ const AlbumGallery = React.forwardRef( } ) -AlbumGallery.propTypes = { - album: PropTypes.object, - loading: PropTypes.bool, - customAlbumLink: PropTypes.func, - showFilter: PropTypes.bool, - setOnlyFavorites: PropTypes.func, - onlyFavorites: PropTypes.bool, - onFavorite: PropTypes.func, - setOrdering: PropTypes.func, - ordering: PropTypes.object, -} - export default AlbumGallery diff --git a/ui/src/components/photoGallery/MediaThumbnail.tsx b/ui/src/components/photoGallery/MediaThumbnail.tsx index 3c583e7..53b5397 100644 --- a/ui/src/components/photoGallery/MediaThumbnail.tsx +++ b/ui/src/components/photoGallery/MediaThumbnail.tsx @@ -119,7 +119,7 @@ type MediaThumbnailProps = { index: number active: boolean setPresenting(presenting: boolean): void - onFavorite(): void + onFavorite?(): void } export const MediaThumbnail = ({ diff --git a/ui/src/components/photoGallery/PhotoGallery.tsx b/ui/src/components/photoGallery/PhotoGallery.tsx index 0cc3474..5d681af 100644 --- a/ui/src/components/photoGallery/PhotoGallery.tsx +++ b/ui/src/components/photoGallery/PhotoGallery.tsx @@ -48,7 +48,7 @@ type PhotoGalleryProps = { setPresenting(presenting: boolean): void nextImage(): void previousImage(): void - onFavorite(): void + onFavorite?(): void } const PhotoGallery = ({ diff --git a/ui/src/components/photoGallery/ProtectedMedia.tsx b/ui/src/components/photoGallery/ProtectedMedia.tsx index 445579e..c53af99 100644 --- a/ui/src/components/photoGallery/ProtectedMedia.tsx +++ b/ui/src/components/photoGallery/ProtectedMedia.tsx @@ -71,7 +71,7 @@ export interface ProtectedVideoProps_Media { thumbnail: null | { url: string } - videoWeb: { + videoWeb: null | { url: string } } @@ -80,14 +80,21 @@ export interface ProtectedVideoProps { media: ProtectedVideoProps_Media } -export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => ( - -) +export const ProtectedVideo = ({ media, ...props }: ProtectedVideoProps) => { + if (media.videoWeb === null) { + console.error('ProetctedVideo called with media.videoWeb = null') + return null + } + + return ( + + ) +} diff --git a/ui/src/components/photoGallery/presentView/PresentMedia.tsx b/ui/src/components/photoGallery/presentView/PresentMedia.tsx index 228e3a7..782f50d 100644 --- a/ui/src/components/photoGallery/presentView/PresentMedia.tsx +++ b/ui/src/components/photoGallery/presentView/PresentMedia.tsx @@ -27,7 +27,7 @@ const StyledVideo = styled(ProtectedVideo)` export interface PresentMediaProps_Media extends ProtectedVideoProps_Media { type: MediaType - highRes?: { + highRes: null | { url: string } } diff --git a/ui/src/helpers/utils.ts b/ui/src/helpers/utils.ts index e543c1b..40446da 100644 --- a/ui/src/helpers/utils.ts +++ b/ui/src/helpers/utils.ts @@ -34,6 +34,6 @@ export function debounce any>( return debounced } -export function isNil(value: any) { +export function isNil(value: any): value is undefined | null { return value === undefined || value === null } diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 547c11a..07e7d9c 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -23,7 +23,7 @@ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true /* Do not emit outputs. */, + "noEmit": true /* Do not emit outputs. */, // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ From f09ba14a0e937b8dc6a2af239bef0f54e50dbe71 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Tue, 13 Apr 2021 16:43:56 +0200 Subject: [PATCH 23/28] Fix translations + fix build reload + more Typescript --- .gitignore | 1 + ui/babel.config.js | 1 - ui/build.mjs | 4 +- ui/extractedTranslations/da/translation.json | 188 +++++++++--------- ui/extractedTranslations/en/translation.json | 8 +- ui/src/App.tsx | 10 + ui/src/Layout.tsx | 4 +- .../{SettingsPage.js => SettingsPage.tsx} | 2 +- ...UserPreferences.js => UserPreferences.tsx} | 26 ++- ui/src/Pages/SettingsPage/Users/UsersTable.js | 2 +- ui/src/components/AlbumTitle.tsx | 2 +- .../components/albumGallery/AlbumGallery.tsx | 7 +- .../photoGallery/MediaThumbnail.tsx | 9 +- .../components/photoGallery/PhotoGallery.tsx | 62 +++--- .../photoGallery/ProtectedMedia.tsx | 6 + ui/src/components/routes/AuthorizedRoute.tsx | 4 +- 16 files changed, 185 insertions(+), 151 deletions(-) rename ui/src/Pages/SettingsPage/{SettingsPage.js => SettingsPage.tsx} (95%) rename ui/src/Pages/SettingsPage/{UserPreferences.js => UserPreferences.tsx} (67%) diff --git a/.gitignore b/.gitignore index 34bbc3a..541190a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ node_modules/ .cache/ dist/ build/ +*.tsbuildinfo # misc .DS_Store diff --git a/ui/babel.config.js b/ui/babel.config.js index 0f6c8a3..38b42c4 100644 --- a/ui/babel.config.js +++ b/ui/babel.config.js @@ -16,7 +16,6 @@ module.exports = function (api) { 'i18next-extract', { locales: ['en', 'da'], - discardOldKeys: true, defaultValue: null, }, ]) diff --git a/ui/build.mjs b/ui/build.mjs index 42f4006..c7b18a1 100644 --- a/ui/build.mjs +++ b/ui/build.mjs @@ -69,7 +69,9 @@ if (watchMode) { bs.watch('src/**/*.@(js|tsx|ts)').on('change', async args => { console.log('reloading', args) builderPromise = (await builderPromise).rebuild() - bs.reload(args) + setTimeout(() => { + bs.reload(args) + }, 1000) }) } else { const build = async () => { diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index 2c3f540..01e60ac 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -1,12 +1,12 @@ { "album_filter": { - "only_favorites": null, - "sort_by": null, + "only_favorites": "Vis kun favoritter", + "sort_by": "Sorter efter", "sorting_options": { - "date_imported": null, - "date_shot": null, - "title": null, - "type": null + "date_imported": "Dato for importering", + "date_shot": "Dato", + "title": "Titel", + "type": "Type" } }, "albums_page": { @@ -14,33 +14,33 @@ }, "general": { "action": { - "add": null, - "cancel": null, - "delete": null, - "remove": null, - "save": null + "add": "Tilføj", + "cancel": "Annuller", + "delete": "Slet", + "remove": "Fjern", + "save": "Gem" }, "loading": { - "album": null, + "album": "Loader album", "default": "Loader...", - "media": null, + "media": "Loader medier", "page": "Loader side", "paginate": { "faces": "Loader flere personer", "media": "Loader flere medier" }, - "shares": null, + "shares": "Loader delinger...", "timeline": "Loader tidslinje" } }, "header": { "search": { - "loading": null, - "no_results": null, - "placeholder": null, + "loading": "Loader resultater...", + "no_results": "Fandt ingen resultater", + "placeholder": "Søg", "result_type": { - "albums": null, - "photos": null + "albums": "Albums", + "photos": "Billeder" } } }, @@ -62,6 +62,9 @@ }, "welcome": "Velkommen til Photoview" }, + "meta": { + "description": null + }, "people_page": { "face_group": { "label_placeholder": "Navn", @@ -74,24 +77,24 @@ }, "settings": { "concurrent_workers": { - "description": null, - "title": null + "description": "Det maksimale antal medier som må skannes samtidig", + "title": "Samtidige scanner-arbejdere" }, "logout": null, "periodic_scanner": { - "checkbox_label": null, + "checkbox_label": "Aktiver periodiske scanner", "field": { - "description": null, - "label": null + "description": "Hvor ofte scanneren bør udføre automatiske scanninger af alle brugere", + "label": "Periodiske scanningsintervaller" }, "interval_unit": { - "days": null, - "hour": null, - "minutes": null, - "months": null, - "seconds": null + "days": "Dage", + "hour": "Timer", + "minutes": "Minutter", + "months": "Måneder", + "seconds": "Sekunder" }, - "title": null + "title": "Periodisk scanner" }, "scanner": { "description": "Vil scanne alle brugere for nye eller opdaterede medier", @@ -106,21 +109,21 @@ }, "users": { "add_user": { - "submit": null + "submit": "Tilføj bruger" }, "confirm_delete_user": { - "action": null, - "description": null, - "title": null + "action": "Slet {user}", + "description": "<0>Er du sikker på at du vil slette <1>?

Denne handling kan ikke fortrydes

", + "title": "Slet bruger" }, "password_reset": { - "description": null, + "description": "Ændre adgangskode for <1>", "form": { - "label": null, - "placeholder": null, - "submit": null + "label": "Ny adgangskode", + "placeholder": "adgangskode", + "submit": "Ændre adgangskode" }, - "title": null + "title": "Ændre adgangskode" }, "table": { "column_names": { @@ -132,10 +135,10 @@ "new_user": "Ny bruger", "row": { "action": { - "change_password": null, - "delete": null, - "edit": null, - "scan": null + "change_password": "Ændre adgangskode", + "delete": "Slet", + "edit": "Rediger", + "scan": "Scan" } } }, @@ -143,6 +146,9 @@ } }, "share_page": { + "media": { + "title": null + }, "protected_share": { "description": "Denne deling er låst med en adgangskode.", "title": "Beskyttet deling" @@ -153,72 +159,72 @@ }, "sidebar": { "album": { - "title": null + "title": "Album indstillinger" }, "download": { "filesize": { - "byte": null, - "byte_plural": null, - "giga_byte": null, - "kilo_byte": null, - "mega_byte": null, - "tera_byte": null + "byte": "{{count}} Byte", + "byte_plural": "{{count}} Bytes", + "giga_byte": "{{count}} GB", + "kilo_byte": "{{count}} KB", + "mega_byte": "{{count}} MB", + "tera_byte": "{{count}} TB" }, "table_columns": { - "dimensions": null, - "file_size": null, - "file_type": null, - "name": null + "dimensions": "Dimension", + "file_size": "Størrelse", + "file_type": "Type", + "name": "Navn" }, - "title": null + "title": "Download" }, "media": { "exif": { "exposure_program": { - "action_program": null, - "aperture_priority": null, - "bulb": null, - "creative_program": null, - "landscape_mode": null, - "manual": null, - "normal_program": null, - "not_defined": null, - "portrait_mode": null, - "shutter_priority": null + "action_program": "Actionprogram", + "aperture_priority": "Blændeprioritet", + "bulb": "Bulb", + "creative_program": "Kreativ program", + "landscape_mode": "Landskabsmode", + "manual": "Manuel", + "normal_program": "Normal program", + "not_defined": "Ikke defineret", + "portrait_mode": "Portræt mode", + "shutter_priority": "Lukkerprioritet" }, "flash": { - "auto": null, - "did_not_fire": null, - "fired": null, - "no_flash": null, - "no_flash_function": null, - "off": null, - "on": null, - "red_eye_reduction": null, - "return_detected": null, - "return_not_detected": null + "auto": "Auto", + "did_not_fire": "Blitz affyrede ikke", + "fired": "Affyrede", + "no_flash": "Ingen blitz", + "no_flash_function": "Ingen blitz-funktion", + "off": "Slukket", + "on": "Tændt", + "red_eye_reduction": "Røde øjne reduktion", + "return_detected": "Retur registreret", + "return_not_detected": "Retur ikke registreret" }, "name": { - "aperture": null, - "camera": null, - "date_shot": null, - "exposure": null, - "exposure_program": null, - "flash": null, - "focal_length": null, - "iso": null, - "lens": null, - "maker": null + "aperture": "Blænde", + "camera": "Kamera", + "date_shot": "Dato", + "exposure": "Lukketid", + "exposure_program": "Lukketid program", + "flash": "Blitz", + "focal_length": "Fokallængde", + "iso": "ISO", + "lens": "Lense", + "maker": "Mærke" } } }, "sharing": { - "add_share": null, - "copy_link": null, - "no_shares_found": null, - "public_link": null, - "table_header": null, - "title": null + "add_share": "Tilføj deling", + "copy_link": "Kopier link", + "no_shares_found": "Ingen delinger fundet", + "public_link": "Offentligt link", + "table_header": "Offentlige delinger", + "title": "Indstillinger for deling" } }, "sidemenu": { diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index d5f801c..b29d397 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -62,6 +62,9 @@ }, "welcome": "Welcome to Photoview" }, + "meta": { + "description": "Simple and User-friendly Photo Gallery for Personal Servers" + }, "people_page": { "face_group": { "label_placeholder": "Label", @@ -143,6 +146,9 @@ } }, "share_page": { + "media": { + "title": "Shared media" + }, "protected_share": { "description": "This share is protected with a password.", "title": "Protected share" @@ -158,7 +164,7 @@ "download": { "filesize": { "byte": "{{count}} Byte", - "byte_plural": null, + "byte_plural": "{{count}} Bytes", "giga_byte": "{{count}} GB", "kilo_byte": "{{count}} KB", "mega_byte": "{{count}} MB", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index dbbac51..25fdb5d 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -44,6 +44,7 @@ const SITE_TRANSLATION = gql` ` const loadTranslations = () => { + console.log('load translation') const [loadLang, { data }] = useLazyQuery(SITE_TRANSLATION) useEffect(() => { @@ -53,13 +54,22 @@ const loadTranslations = () => { }, [authToken()]) useEffect(() => { + console.log('loading translations', data) switch (data?.myUserPreferences.language) { case LanguageTranslation.Danish: import('../extractedTranslations/da/translation.json').then(danish => { + console.log('loading danish') i18n.addResourceBundle('da', 'translation', danish) i18n.changeLanguage('da') }) break + case LanguageTranslation.English: + import('../extractedTranslations/en/translation.json').then(english => { + console.log('loading english') + i18n.addResourceBundle('en', 'translation', english) + i18n.changeLanguage('en') + }) + break default: i18n.changeLanguage('en') } diff --git a/ui/src/Layout.tsx b/ui/src/Layout.tsx index bcb2241..4ca3f16 100644 --- a/ui/src/Layout.tsx +++ b/ui/src/Layout.tsx @@ -1,4 +1,4 @@ -import React, { ReactChild, ReactChildren } from 'react' +import React, { ReactChild } from 'react' import PropTypes from 'prop-types' import styled from 'styled-components' import { NavLink } from 'react-router-dom' @@ -143,7 +143,7 @@ export const SideMenu = () => { } type LayoutProps = { - children: ReactChild | ReactChildren + children: React.ReactNode title: string } diff --git a/ui/src/Pages/SettingsPage/SettingsPage.js b/ui/src/Pages/SettingsPage/SettingsPage.tsx similarity index 95% rename from ui/src/Pages/SettingsPage/SettingsPage.js rename to ui/src/Pages/SettingsPage/SettingsPage.tsx index 31b80bf..b9d0269 100644 --- a/ui/src/Pages/SettingsPage/SettingsPage.js +++ b/ui/src/Pages/SettingsPage/SettingsPage.tsx @@ -10,7 +10,7 @@ import ScannerSection from './ScannerSection' import UserPreferences from './UserPreferences' import UsersTable from './Users/UsersTable' -export const SectionTitle = styled.h2` +export const SectionTitle = styled.h2<{ nospace: boolean }>` margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important; padding-bottom: 0.3em; border-bottom: 1px solid #ddd; diff --git a/ui/src/Pages/SettingsPage/UserPreferences.js b/ui/src/Pages/SettingsPage/UserPreferences.tsx similarity index 67% rename from ui/src/Pages/SettingsPage/UserPreferences.js rename to ui/src/Pages/SettingsPage/UserPreferences.tsx index b67d6d0..3149286 100644 --- a/ui/src/Pages/SettingsPage/UserPreferences.js +++ b/ui/src/Pages/SettingsPage/UserPreferences.tsx @@ -4,12 +4,17 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { Dropdown } from 'semantic-ui-react' import styled from 'styled-components' - +import { LanguageTranslation } from '../../../__generated__/globalTypes' import { SectionTitle } from './SettingsPage' +import { + changeUserPreferences, + changeUserPreferencesVariables, +} from './__generated__/changeUserPreferences' +import { myUserPreferences } from './__generated__/myUserPreferences' const languagePreferences = [ - { key: 1, text: 'English', value: 'en' }, - { key: 2, text: 'Dansk', value: 'da' }, + { key: 1, text: 'English', value: LanguageTranslation.English }, + { key: 2, text: 'Dansk', value: LanguageTranslation.Danish }, ] const CHANGE_USER_PREFERENCES = gql` @@ -37,14 +42,15 @@ const UserPreferencesWrapper = styled.div` const UserPreferences = () => { const { t } = useTranslation() - const { data } = useQuery(MY_USER_PREFERENCES) + const { data } = useQuery(MY_USER_PREFERENCES) - const [changePrefs, { loading: loadingPrefs, error }] = useMutation( - CHANGE_USER_PREFERENCES - ) + const [changePrefs, { loading: loadingPrefs, error }] = useMutation< + changeUserPreferences, + changeUserPreferencesVariables + >(CHANGE_USER_PREFERENCES) if (error) { - return error.message + return
{error.message}
} return ( @@ -62,12 +68,12 @@ const UserPreferences = () => { onChange={(event, { value: language }) => { changePrefs({ variables: { - language, + language: language as LanguageTranslation, }, }) }} selection - value={data?.myUserPreferences.language} + value={data?.myUserPreferences.language || undefined} loading={loadingPrefs} disabled={loadingPrefs} /> diff --git a/ui/src/Pages/SettingsPage/Users/UsersTable.js b/ui/src/Pages/SettingsPage/Users/UsersTable.js index 90b4ca1..4d7f21f 100644 --- a/ui/src/Pages/SettingsPage/Users/UsersTable.js +++ b/ui/src/Pages/SettingsPage/Users/UsersTable.js @@ -29,7 +29,7 @@ const UsersTable = () => { const { loading, error, data, refetch } = useQuery(USERS_QUERY) if (error) { - return `Users table error: ${error.message}` + return
{`Users table error: ${error.message}`}
} let userRows = [] diff --git a/ui/src/components/AlbumTitle.tsx b/ui/src/components/AlbumTitle.tsx index b7fd32b..4cf5604 100644 --- a/ui/src/components/AlbumTitle.tsx +++ b/ui/src/components/AlbumTitle.tsx @@ -50,7 +50,7 @@ const ALBUM_PATH_QUERY = gql` ` type AlbumTitleProps = { - album: { + album?: { id: string title: string } diff --git a/ui/src/components/albumGallery/AlbumGallery.tsx b/ui/src/components/albumGallery/AlbumGallery.tsx index 4c9fd92..5eeb1a6 100644 --- a/ui/src/components/albumGallery/AlbumGallery.tsx +++ b/ui/src/components/albumGallery/AlbumGallery.tsx @@ -6,7 +6,7 @@ import AlbumFilter from '../AlbumFilter' import { albumQuery_album } from '../../Pages/AlbumPage/__generated__/albumQuery' type AlbumGalleryProps = { - album: albumQuery_album + album?: albumQuery_album loading?: boolean customAlbumLink?(albumID: string): string showFilter?: boolean @@ -64,10 +64,13 @@ const AlbumGallery = React.forwardRef( } const nextImage = () => { + if (album === undefined) return setActiveImage((imageState.activeImage + 1) % album.media.length) } const previousImage = () => { + if (album === undefined) return + if (imageState.activeImage <= 0) { setActiveImage(album.media.length - 1) } else { @@ -131,7 +134,7 @@ const AlbumGallery = React.forwardRef( } { diff --git a/ui/src/components/photoGallery/MediaThumbnail.tsx b/ui/src/components/photoGallery/MediaThumbnail.tsx index 53b5397..2ddb98c 100644 --- a/ui/src/components/photoGallery/MediaThumbnail.tsx +++ b/ui/src/components/photoGallery/MediaThumbnail.tsx @@ -4,6 +4,10 @@ import styled from 'styled-components' import { Icon } from 'semantic-ui-react' import { ProtectedImage } from './ProtectedMedia' import { MediaType } from '../../../__generated__/globalTypes' +import { + markMediaFavorite, + markMediaFavoriteVariables, +} from './__generated__/markMediaFavorite' const markFavoriteMutation = gql` mutation markMediaFavorite($mediaId: ID!, $favorite: Boolean!) { @@ -130,7 +134,10 @@ export const MediaThumbnail = ({ setPresenting, onFavorite, }: MediaThumbnailProps) => { - const [markFavorite] = useMutation(markFavoriteMutation) + const [markFavorite] = useMutation< + markMediaFavorite, + markMediaFavoriteVariables + >(markFavoriteMutation) let heartIcon = null if (media.favorite !== undefined) { diff --git a/ui/src/components/photoGallery/PhotoGallery.tsx b/ui/src/components/photoGallery/PhotoGallery.tsx index 5d681af..61d140c 100644 --- a/ui/src/components/photoGallery/PhotoGallery.tsx +++ b/ui/src/components/photoGallery/PhotoGallery.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components' import { Loader } from 'semantic-ui-react' import { MediaThumbnail, PhotoThumbnail } from './MediaThumbnail' import PresentView from './presentView/PresentView' -import { SidebarContext, UpdateSidebarFn } from '../sidebar/Sidebar' +import { SidebarContext } from '../sidebar/Sidebar' import MediaSidebar from '../sidebar/MediaSidebar' import { useTranslation } from 'react-i18next' import { PresentMediaProps_Media } from './presentView/PresentMedia' @@ -65,44 +65,32 @@ const PhotoGallery = ({ const { t } = useTranslation() const { updateSidebar } = useContext(SidebarContext) - if ( - media === undefined || - activeIndex === -1 || - media[activeIndex] === undefined - ) { - return null - } + const activeImage: PhotoGalleryProps_Media | undefined = media[activeIndex] - const activeImage = media[activeIndex] + let photoElements = [] + if (media) { + photoElements = media.map((photo, index) => { + const active = activeIndex == index - const getPhotoElements = (updateSidebar: UpdateSidebarFn) => { - let photoElements = [] - if (media) { - photoElements = media.map((photo, index) => { - const active = activeIndex == index - - return ( - { - updateSidebar() - onSelectImage(index) - }} - onFavorite={onFavorite} - setPresenting={setPresenting} - index={index} - active={active} - /> - ) - }) - } else { - for (let i = 0; i < 6; i++) { - photoElements.push() - } + return ( + { + updateSidebar() + onSelectImage(index) + }} + onFavorite={onFavorite} + setPresenting={setPresenting} + index={index} + active={active} + /> + ) + }) + } else { + for (let i = 0; i < 6; i++) { + photoElements.push() } - - return photoElements } return ( @@ -111,7 +99,7 @@ const PhotoGallery = ({ {t('general.loading.media', 'Loading media')} - {getPhotoElements(updateSidebar)} + {photoElements} {presenting && ( diff --git a/ui/src/components/photoGallery/ProtectedMedia.tsx b/ui/src/components/photoGallery/ProtectedMedia.tsx index c53af99..87eaad7 100644 --- a/ui/src/components/photoGallery/ProtectedMedia.tsx +++ b/ui/src/components/photoGallery/ProtectedMedia.tsx @@ -26,6 +26,7 @@ export interface ProtectedImageProps src?: string key?: string lazyLoading?: boolean + loaded?: boolean } /** @@ -38,6 +39,7 @@ export const ProtectedImage = ({ src, key, lazyLoading, + loaded, ...props }: ProtectedImageProps) => { const lazyLoadProps: { 'data-src'?: string; loading?: 'lazy' | 'eager' } = {} @@ -55,11 +57,15 @@ export const ProtectedImage = ({ ? placeholder : getProtectedUrl(src) || placeholder + const loadedProp = + loaded !== undefined ? { loaded: loaded.toString() } : undefined + return ( diff --git a/ui/src/components/routes/AuthorizedRoute.tsx b/ui/src/components/routes/AuthorizedRoute.tsx index 6779f3d..6c0c6a4 100644 --- a/ui/src/components/routes/AuthorizedRoute.tsx +++ b/ui/src/components/routes/AuthorizedRoute.tsx @@ -6,10 +6,10 @@ import { authToken } from '../../helpers/authentication' import { ADMIN_QUERY } from '../../Layout' export const useIsAdmin = (enabled = true) => { - const [fetchAdminQuery, { data }] = useLazyQuery(ADMIN_QUERY) + const [fetchAdminQuery, { data, called }] = useLazyQuery(ADMIN_QUERY) useEffect(() => { - if (authToken() && !data && enabled) { + if (authToken() && !called && enabled) { fetchAdminQuery() } }, [authToken(), enabled]) From 99a98c0208f556bc52c9bb7c4df1dff256a31461 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Tue, 13 Apr 2021 16:44:41 +0200 Subject: [PATCH 24/28] Add new danish translations --- ui/extractedTranslations/da/translation.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index 01e60ac..a385d60 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -63,7 +63,7 @@ "welcome": "Velkommen til Photoview" }, "meta": { - "description": null + "description": "Simpelt og Brugervenligt Photo-galleri for Personlige Servere" }, "people_page": { "face_group": { @@ -80,7 +80,7 @@ "description": "Det maksimale antal medier som må skannes samtidig", "title": "Samtidige scanner-arbejdere" }, - "logout": null, + "logout": "Log ud", "periodic_scanner": { "checkbox_label": "Aktiver periodiske scanner", "field": { @@ -147,7 +147,7 @@ }, "share_page": { "media": { - "title": null + "title": "Delt medie" }, "protected_share": { "description": "Denne deling er låst med en adgangskode.", From 97a8f3726d885a48d4faedc8d57f3ae4da3f5f7d Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Tue, 13 Apr 2021 19:57:33 +0200 Subject: [PATCH 25/28] Convert many pages to Typescript --- api/graphql/generated.go | 87 +++++++--- api/graphql/schema.graphql | 16 +- ui/babel.config.js | 5 +- ui/build.mjs | 4 +- ui/extractedTranslations/da/translation.json | 9 +- ui/extractedTranslations/en/translation.json | 7 + .../AlbumPage/{AlbumPage.js => AlbumPage.tsx} | 56 ++++--- .../{AlbumsPage.js => AlbumsPage.tsx} | 0 ...itialSetupPage.js => InitialSetupPage.tsx} | 50 +++--- .../LoginPage/{LoginPage.js => LoginPage.tsx} | 4 +- .../{loginUtilities.js => loginUtilities.tsx} | 4 +- .../{PhotosPage.js => PhotosPage.tsx} | 14 +- ...PeriodicScanner.js => PeriodicScanner.tsx} | 105 +++++++----- ...orkers.js => ScannerConcurrentWorkers.tsx} | 37 +++-- .../{ScannerSection.js => ScannerSection.tsx} | 3 +- ui/src/Pages/SettingsPage/SettingsPage.tsx | 2 +- ui/src/Pages/SettingsPage/UserPreferences.tsx | 26 ++- .../Users/{AddUserRow.js => AddUserRow.tsx} | 30 ++-- .../Users/{EditUserRow.js => EditUserRow.tsx} | 13 +- ...wRootPaths.js => EditUserRowRootPaths.tsx} | 85 +++++----- ...angePassword.js => UserChangePassword.tsx} | 24 +-- ui/src/Pages/SettingsPage/Users/UserRow.js | 111 ------------- ui/src/Pages/SettingsPage/Users/UserRow.tsx | 153 ++++++++++++++++++ .../Users/{UsersTable.js => UsersTable.tsx} | 10 +- .../Users/{ViewUserRow.js => ViewUserRow.tsx} | 10 +- .../Users/__generated__/changeUserPassword.ts | 10 +- .../Users/__generated__/createUser.ts | 14 +- .../Users/__generated__/deleteUser.ts | 10 +- .../Users/__generated__/updateUser.ts | 16 +- ui/src/components/albumGallery/AlbumBox.tsx | 7 +- .../{AlbumBoxes.js => AlbumBoxes.tsx} | 20 +-- .../components/albumGallery/AlbumGallery.tsx | 3 +- .../__generated__/markMediaFavorite.ts | 12 +- ui/src/components/routes/Routes.test.js | 2 +- .../__generated__/sidebarAlbumAddShare.ts | 12 +- .../__generated__/sidebarPhotoAddShare.ts | 12 +- .../__generated__/sidebarProtectShare.ts | 12 +- .../__generated__/sidebareDeleteShare.ts | 8 +- .../timelineGallery/TimelineGallery.js | 2 +- ui/src/helpers/LazyLoad.ts | 2 +- ui/src/hooks/useScrollPagination.ts | 9 +- ui/src/hooks/useURLParameters.ts | 6 +- 42 files changed, 613 insertions(+), 409 deletions(-) rename ui/src/Pages/AlbumPage/{AlbumPage.js => AlbumPage.tsx} (78%) rename ui/src/Pages/AllAlbumsPage/{AlbumsPage.js => AlbumsPage.tsx} (100%) rename ui/src/Pages/LoginPage/{InitialSetupPage.js => InitialSetupPage.tsx} (90%) rename ui/src/Pages/LoginPage/{LoginPage.js => LoginPage.tsx} (96%) rename ui/src/Pages/LoginPage/{loginUtilities.js => loginUtilities.tsx} (86%) rename ui/src/Pages/PhotosPage/{PhotosPage.js => PhotosPage.tsx} (58%) rename ui/src/Pages/SettingsPage/{PeriodicScanner.js => PeriodicScanner.tsx} (68%) rename ui/src/Pages/SettingsPage/{ScannerConcurrentWorkers.js => ScannerConcurrentWorkers.tsx} (68%) rename ui/src/Pages/SettingsPage/{ScannerSection.js => ScannerSection.tsx} (89%) rename ui/src/Pages/SettingsPage/Users/{AddUserRow.js => AddUserRow.tsx} (86%) rename ui/src/Pages/SettingsPage/Users/{EditUserRow.js => EditUserRow.tsx} (87%) rename ui/src/Pages/SettingsPage/Users/{EditUserRowRootPaths.js => EditUserRowRootPaths.tsx} (65%) rename ui/src/Pages/SettingsPage/Users/{UserChangePassword.js => UserChangePassword.tsx} (82%) delete mode 100644 ui/src/Pages/SettingsPage/Users/UserRow.js create mode 100644 ui/src/Pages/SettingsPage/Users/UserRow.tsx rename ui/src/Pages/SettingsPage/Users/{UsersTable.js => UsersTable.tsx} (91%) rename ui/src/Pages/SettingsPage/Users/{ViewUserRow.js => ViewUserRow.tsx} (92%) rename ui/src/components/albumGallery/{AlbumBoxes.js => AlbumBoxes.tsx} (60%) diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 03d8520..cdad8e4 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -1715,29 +1715,29 @@ type Mutation { scanUser(userId: ID!): ScannerResult! @isAdmin "Generate share token for album" - shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized + shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken! @isAuthorized "Generate share token for media" - shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized + shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken! @isAuthorized "Delete a share token by it's token value" - deleteShareToken(token: String!): ShareToken @isAuthorized + deleteShareToken(token: String!): ShareToken! @isAuthorized "Set a password for a token, if null is passed for the password argument, the password will be cleared" - protectShareToken(token: String!, password: String): ShareToken @isAuthorized + protectShareToken(token: String!, password: String): ShareToken! @isAuthorized "Mark or unmark a media as being a favorite" - favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized + favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized updateUser( id: ID! username: String password: String admin: Boolean - ): User @isAdmin + ): User! @isAdmin createUser( username: String! password: String admin: Boolean! - ): User @isAdmin - deleteUser(id: ID!): User @isAdmin + ): User! @isAdmin + deleteUser(id: ID!): User! @isAdmin "Add a root path from where to look for media for the given user" userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin @@ -5084,11 +5084,14 @@ func (ec *executionContext) _Mutation_shareAlbum(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.ShareToken) fc.Result = res - return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5143,11 +5146,14 @@ func (ec *executionContext) _Mutation_shareMedia(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.ShareToken) fc.Result = res - return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5202,11 +5208,14 @@ func (ec *executionContext) _Mutation_deleteShareToken(ctx context.Context, fiel return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.ShareToken) fc.Result = res - return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5261,11 +5270,14 @@ func (ec *executionContext) _Mutation_protectShareToken(ctx context.Context, fie return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.ShareToken) fc.Result = res - return ec.marshalOShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) + return ec.marshalNShareToken2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5320,11 +5332,14 @@ func (ec *executionContext) _Mutation_favoriteMedia(ctx context.Context, field g return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.Media) fc.Result = res - return ec.marshalOMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res) + return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5379,11 +5394,14 @@ func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.User) fc.Result = res - return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5438,11 +5456,14 @@ func (ec *executionContext) _Mutation_createUser(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.User) fc.Result = res - return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -5497,11 +5518,14 @@ func (ec *executionContext) _Mutation_deleteUser(ctx context.Context, field grap return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.(*models.User) fc.Result = res - return ec.marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) + return ec.marshalNUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx, field.Selections, res) } func (ec *executionContext) _Mutation_userAddRootPath(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -10520,20 +10544,44 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) } case "shareAlbum": out.Values[i] = ec._Mutation_shareAlbum(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "shareMedia": out.Values[i] = ec._Mutation_shareMedia(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "deleteShareToken": out.Values[i] = ec._Mutation_deleteShareToken(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "protectShareToken": out.Values[i] = ec._Mutation_protectShareToken(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "favoriteMedia": out.Values[i] = ec._Mutation_favoriteMedia(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "updateUser": out.Values[i] = ec._Mutation_updateUser(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "createUser": out.Values[i] = ec._Mutation_createUser(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "deleteUser": out.Values[i] = ec._Mutation_deleteUser(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "userAddRootPath": out.Values[i] = ec._Mutation_userAddRootPath(ctx, field) case "userRemoveRootAlbum": @@ -12685,13 +12733,6 @@ func (ec *executionContext) marshalOTime2ᚖtimeᚐTime(ctx context.Context, sel return graphql.MarshalTime(*v) } -func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUser(ctx context.Context, sel ast.SelectionSet, v *models.User) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._User(ctx, sel, v) -} - func (ec *executionContext) marshalOVideoMetadata2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐVideoMetadata(ctx context.Context, sel ast.SelectionSet, v *models.VideoMetadata) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index bf81823..ab085d7 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -95,29 +95,29 @@ type Mutation { scanUser(userId: ID!): ScannerResult! @isAdmin "Generate share token for album" - shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken @isAuthorized + shareAlbum(albumId: ID!, expire: Time, password: String): ShareToken! @isAuthorized "Generate share token for media" - shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken @isAuthorized + shareMedia(mediaId: ID!, expire: Time, password: String): ShareToken! @isAuthorized "Delete a share token by it's token value" - deleteShareToken(token: String!): ShareToken @isAuthorized + deleteShareToken(token: String!): ShareToken! @isAuthorized "Set a password for a token, if null is passed for the password argument, the password will be cleared" - protectShareToken(token: String!, password: String): ShareToken @isAuthorized + protectShareToken(token: String!, password: String): ShareToken! @isAuthorized "Mark or unmark a media as being a favorite" - favoriteMedia(mediaId: ID!, favorite: Boolean!): Media @isAuthorized + favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized updateUser( id: ID! username: String password: String admin: Boolean - ): User @isAdmin + ): User! @isAdmin createUser( username: String! password: String admin: Boolean! - ): User @isAdmin - deleteUser(id: ID!): User @isAdmin + ): User! @isAdmin + deleteUser(id: ID!): User! @isAdmin "Add a root path from where to look for media for the given user" userAddRootPath(id: ID!, rootPath: String!): Album @isAdmin diff --git a/ui/babel.config.js b/ui/babel.config.js index 38b42c4..8967ba4 100644 --- a/ui/babel.config.js +++ b/ui/babel.config.js @@ -9,8 +9,6 @@ module.exports = function (api) { presets.push('@babel/preset-env') plugins.push('@babel/plugin-transform-runtime') } else { - plugins.push(['styled-components', { pure: true }]) - plugins.push('graphql-tag') if (!isProduction) { plugins.push([ 'i18next-extract', @@ -20,6 +18,9 @@ module.exports = function (api) { }, ]) } + + plugins.push(['styled-components', { pure: true }]) + plugins.push('graphql-tag') } return { diff --git a/ui/build.mjs b/ui/build.mjs index c7b18a1..e8efb7b 100644 --- a/ui/build.mjs +++ b/ui/build.mjs @@ -69,9 +69,7 @@ if (watchMode) { bs.watch('src/**/*.@(js|tsx|ts)').on('change', async args => { console.log('reloading', args) builderPromise = (await builderPromise).rebuild() - setTimeout(() => { - bs.reload(args) - }, 1000) + // bs.reload(args) }) } else { const build = async () => { diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json index a385d60..5c10625 100644 --- a/ui/extractedTranslations/da/translation.json +++ b/ui/extractedTranslations/da/translation.json @@ -72,6 +72,9 @@ }, "recognize_unlabeled_faces_button": "Genkend ikke navngivede ansigter" }, + "photos_page": { + "title": "Billeder" + }, "routes": { "page_not_found": "Side ikke fundet" }, @@ -102,6 +105,10 @@ "title": "Scanner" }, "user_preferences": { + "change_language": { + "description": "Set sidens sprog specifikt for denne bruger", + "label": "Sprog" + }, "language_selector": { "placeholder": "Vælg sprog" }, @@ -211,7 +218,7 @@ "exposure": "Lukketid", "exposure_program": "Lukketid program", "flash": "Blitz", - "focal_length": "Fokallængde", + "focal_length": "Brændvidde", "iso": "ISO", "lens": "Lense", "maker": "Mærke" diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json index b29d397..09f8ced 100644 --- a/ui/extractedTranslations/en/translation.json +++ b/ui/extractedTranslations/en/translation.json @@ -72,6 +72,9 @@ }, "recognize_unlabeled_faces_button": "Recognize unlabeled faces" }, + "photos_page": { + "title": "Photos" + }, "routes": { "page_not_found": "Page not found" }, @@ -102,6 +105,10 @@ "title": "Scanner" }, "user_preferences": { + "change_language": { + "description": "Change website language specific for this user", + "label": "Website language" + }, "language_selector": { "placeholder": "Select language" }, diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.tsx similarity index 78% rename from ui/src/Pages/AlbumPage/AlbumPage.js rename to ui/src/Pages/AlbumPage/AlbumPage.tsx index 01527b2..960111c 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.js +++ b/ui/src/Pages/AlbumPage/AlbumPage.tsx @@ -1,16 +1,16 @@ import React, { useCallback, useEffect } from 'react' -import ReactRouterPropTypes from 'react-router-prop-types' import { useQuery, gql } from '@apollo/client' import AlbumGallery from '../../components/albumGallery/AlbumGallery' -import PropTypes from 'prop-types' import Layout from '../../Layout' -import useURLParameters from '../../hooks/useURLParameters' +import useURLParameters, { UrlKeyValuePair } from '../../hooks/useURLParameters' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../../components/PaginateLoader' import LazyLoad from '../../helpers/LazyLoad' import { useTranslation } from 'react-i18next' +import { albumQuery, albumQueryVariables } from './__generated__/albumQuery' +import { OrderDirection } from '../../../__generated__/globalTypes' -const albumQuery = gql` +const ALBUM_QUERY = gql` query albumQuery( $id: ID! $onlyFavorites: Boolean @@ -61,7 +61,16 @@ const albumQuery = gql` let refetchNeededAll = false let refetchNeededFavorites = false -function AlbumPage({ match }) { +type AlbumPageProps = { + match: { + params: { + id: string + subPage: string + } + } +} + +function AlbumPage({ match }: AlbumPageProps) { const albumId = match.params.id const { t } = useTranslation() @@ -69,14 +78,22 @@ function AlbumPage({ match }) { const { getParam, setParam, setParams } = useURLParameters() const onlyFavorites = getParam('favorites') == '1' ? true : false - const setOnlyFavorites = favorites => setParam('favorites', favorites ? 1 : 0) + const setOnlyFavorites = (favorites: boolean) => + setParam('favorites', favorites ? '1' : '0') const orderBy = getParam('orderBy', 'date_shot') - const orderDirection = getParam('orderDirection', 'ASC') - const setOrdering = useCallback( + const orderDirStr = getParam('orderDirection', 'ASC') || 'hello' + const orderDirection = orderDirStr as OrderDirection + + type setOrderingFn = (args: { + orderBy?: string + orderDirection?: OrderDirection + }) => void + + const setOrdering: setOrderingFn = useCallback( ({ orderBy, orderDirection }) => { - let updatedParams = [] + const updatedParams: UrlKeyValuePair[] = [] if (orderBy !== undefined) { updatedParams.push({ key: 'orderBy', value: orderBy }) } @@ -89,7 +106,10 @@ function AlbumPage({ match }) { [setParams] ) - const { loading, error, data, refetch, fetchMore } = useQuery(albumQuery, { + const { loading, error, data, refetch, fetchMore } = useQuery< + albumQuery, + albumQueryVariables + >(ALBUM_QUERY, { variables: { id: albumId, onlyFavorites, @@ -100,7 +120,10 @@ function AlbumPage({ match }) { }, }) - const { containerElem, finished: finishedLoadingMore } = useScrollPagination({ + const { + containerElem, + finished: finishedLoadingMore, + } = useScrollPagination({ loading, fetchMore, data, @@ -151,7 +174,6 @@ function AlbumPage({ match }) { ref={containerElem} album={data && data.album} loading={loading} - showFavoritesToggle setOnlyFavorites={toggleFavorites} onlyFavorites={onlyFavorites} onFavorite={() => (refetchNeededAll = refetchNeededFavorites = true)} @@ -167,14 +189,4 @@ function AlbumPage({ match }) { ) } -AlbumPage.propTypes = { - ...ReactRouterPropTypes, - match: PropTypes.shape({ - params: PropTypes.shape({ - id: PropTypes.string, - subPage: PropTypes.string, - }), - }), -} - export default AlbumPage diff --git a/ui/src/Pages/AllAlbumsPage/AlbumsPage.js b/ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx similarity index 100% rename from ui/src/Pages/AllAlbumsPage/AlbumsPage.js rename to ui/src/Pages/AllAlbumsPage/AlbumsPage.tsx diff --git a/ui/src/Pages/LoginPage/InitialSetupPage.js b/ui/src/Pages/LoginPage/InitialSetupPage.tsx similarity index 90% rename from ui/src/Pages/LoginPage/InitialSetupPage.js rename to ui/src/Pages/LoginPage/InitialSetupPage.tsx index 4be5b51..6f879d2 100644 --- a/ui/src/Pages/LoginPage/InitialSetupPage.js +++ b/ui/src/Pages/LoginPage/InitialSetupPage.tsx @@ -7,6 +7,7 @@ import { Container } from './loginUtilities' import { checkInitialSetupQuery, login } from './loginUtilities' import { authToken } from '../../helpers/authentication' import { useTranslation } from 'react-i18next' +import { CheckInitialSetup } from './__generated__/CheckInitialSetup' const initialSetupMutation = gql` mutation InitialSetup( @@ -35,31 +36,13 @@ const InitialSetupPage = () => { rootPath: '', }) - const handleChange = (event, key) => { - const value = event.target.value - setState(prevState => ({ - ...prevState, - [key]: value, - })) - } - - const signIn = (event, authorize) => { - event.preventDefault() - - authorize({ - variables: { - username: state.username, - password: state.password, - rootPath: state.rootPath, - }, - }) - } - if (authToken()) { return } - const { data: initialSetupData } = useQuery(checkInitialSetupQuery) + const { data: initialSetupData } = useQuery( + checkInitialSetupQuery + ) const initialSetupRedirect = initialSetupData?.siteInfo ?.initialSetup ? null : ( @@ -78,6 +61,29 @@ const InitialSetupPage = () => { }, }) + const handleChange = ( + event: React.ChangeEvent, + key: string + ) => { + const value = event.target.value + setState(prevState => ({ + ...prevState, + [key]: value, + })) + } + + const signIn = (event: React.FormEvent) => { + event.preventDefault() + + authorize({ + variables: { + username: state.username, + password: state.password, + rootPath: state.rootPath, + }, + }) + } + let errorMessage = null if (authorizationData && !authorizationData.initialSetupWizard.success) { errorMessage = authorizationData.initialSetupWizard.status @@ -93,7 +99,7 @@ const InitialSetupPage = () => { signIn(e, authorize)} + onSubmit={signIn} loading={ authorizeLoading || authorizationData?.initialSetupWizard?.success } diff --git a/ui/src/Pages/LoginPage/LoginPage.js b/ui/src/Pages/LoginPage/LoginPage.tsx similarity index 96% rename from ui/src/Pages/LoginPage/LoginPage.js rename to ui/src/Pages/LoginPage/LoginPage.tsx index 753fd74..389f182 100644 --- a/ui/src/Pages/LoginPage/LoginPage.js +++ b/ui/src/Pages/LoginPage/LoginPage.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react' import { useQuery, gql, useMutation } from '@apollo/client' import { Redirect } from 'react-router-dom' import styled from 'styled-components' -import { Button, Form, Message, Header } from 'semantic-ui-react' +import { Button, Form, Message, Header, HeaderProps } from 'semantic-ui-react' import { checkInitialSetupQuery, login, Container } from './loginUtilities' import { authToken } from '../../helpers/authentication' @@ -23,7 +23,7 @@ const StyledLogo = styled.img` max-height: 128px; ` -const LogoHeader = props => { +const LogoHeader = (props: HeaderProps) => { const { t } = useTranslation() return ( diff --git a/ui/src/Pages/LoginPage/loginUtilities.js b/ui/src/Pages/LoginPage/loginUtilities.tsx similarity index 86% rename from ui/src/Pages/LoginPage/loginUtilities.js rename to ui/src/Pages/LoginPage/loginUtilities.tsx index 32f713c..06eb8ab 100644 --- a/ui/src/Pages/LoginPage/loginUtilities.js +++ b/ui/src/Pages/LoginPage/loginUtilities.tsx @@ -11,9 +11,9 @@ export const checkInitialSetupQuery = gql` } ` -export function login(token) { +export function login(token: string) { saveTokenCookie(token) - window.location = '/' + window.location.href = '/' } export const Container = styled(SemanticContainer)` diff --git a/ui/src/Pages/PhotosPage/PhotosPage.js b/ui/src/Pages/PhotosPage/PhotosPage.tsx similarity index 58% rename from ui/src/Pages/PhotosPage/PhotosPage.js rename to ui/src/Pages/PhotosPage/PhotosPage.tsx index 4f0b0fa..6da5e95 100644 --- a/ui/src/Pages/PhotosPage/PhotosPage.js +++ b/ui/src/Pages/PhotosPage/PhotosPage.tsx @@ -1,24 +1,18 @@ import React from 'react' import Layout from '../../Layout' -import PropTypes from 'prop-types' import TimelineGallery from '../../components/timelineGallery/TimelineGallery' +import { useTranslation } from 'react-i18next' const PhotosPage = () => { + const { t } = useTranslation() + return ( <> - + ) } -PhotosPage.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape({ - subPage: PropTypes.string, - }), - }), -} - export default PhotosPage diff --git a/ui/src/Pages/SettingsPage/PeriodicScanner.js b/ui/src/Pages/SettingsPage/PeriodicScanner.tsx similarity index 68% rename from ui/src/Pages/SettingsPage/PeriodicScanner.js rename to ui/src/Pages/SettingsPage/PeriodicScanner.tsx index d622890..7e22eae 100644 --- a/ui/src/Pages/SettingsPage/PeriodicScanner.js +++ b/ui/src/Pages/SettingsPage/PeriodicScanner.tsx @@ -4,6 +4,11 @@ import { useMutation, useQuery } from '@apollo/client' import { Checkbox, Dropdown, Input, Loader } from 'semantic-ui-react' import { InputLabelDescription, InputLabelTitle } from './SettingsPage' import { useTranslation } from 'react-i18next' +import { scanIntervalQuery } from './__generated__/scanIntervalQuery' +import { + changeScanIntervalMutation, + changeScanIntervalMutationVariables, +} from './__generated__/changeScanIntervalMutation' const SCAN_INTERVAL_QUERY = gql` query scanIntervalQuery { @@ -19,44 +24,57 @@ const SCAN_INTERVAL_MUTATION = gql` } ` +enum TimeUnit { + Second = 'second', + Minute = 'minute', + Hour = 'hour', + Day = 'day', + Month = 'month', +} + const timeUnits = [ { - value: 'second', + value: TimeUnit.Second, multiplier: 1, }, { - value: 'minute', + value: TimeUnit.Minute, multiplier: 60, }, { - value: 'hour', + value: TimeUnit.Hour, multiplier: 60 * 60, }, { - value: 'day', + value: TimeUnit.Day, multiplier: 60 * 60 * 24, }, { - value: 'month', + value: TimeUnit.Month, multiplier: 60 * 60 * 24 * 30, }, ] -const convertToSeconds = ({ value, unit }) => { - return parseInt(value * timeUnits.find(x => x.value == unit).multiplier) +type TimeValue = { + value: number + unit: TimeUnit } -const convertToAppropriateUnit = ({ value, unit }) => { +const convertToSeconds = ({ value, unit }: TimeValue) => { + return value * (timeUnits.find(x => x.value == unit)?.multiplier as number) +} + +const convertToAppropriateUnit = ({ value, unit }: TimeValue): TimeValue => { if (value == 0) { return { - unit: 'second', + unit: TimeUnit.Second, value: 0, } } const seconds = convertToSeconds({ value, unit }) - let resultingUnit = timeUnits.first + let resultingUnit = timeUnits[0] for (const unit of timeUnits) { if ( seconds / unit.multiplier >= 1 && @@ -78,26 +96,26 @@ const PeriodicScanner = () => { const { t } = useTranslation() const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false) - const [scanInterval, setScanInterval] = useState({ + const [scanInterval, setScanInterval] = useState({ value: 4, - unit: 'minute', + unit: TimeUnit.Minute, }) - const scanIntervalServerValue = useRef(null) + const scanIntervalServerValue = useRef(null) - const scanIntervalQuery = useQuery(SCAN_INTERVAL_QUERY, { + const scanIntervalQuery = useQuery(SCAN_INTERVAL_QUERY, { onCompleted(data) { const queryScanInterval = data.siteInfo.periodicScanInterval if (queryScanInterval == 0) { setScanInterval({ - unit: 'second', - value: '', + unit: TimeUnit.Second, + value: 0, }) } else { setScanInterval( convertToAppropriateUnit({ - unit: 'second', + unit: TimeUnit.Second, value: queryScanInterval, }) ) @@ -110,15 +128,20 @@ const PeriodicScanner = () => { const [ setScanIntervalMutation, { loading: scanIntervalMutationLoading }, - ] = useMutation(SCAN_INTERVAL_MUTATION) + ] = useMutation< + changeScanIntervalMutation, + changeScanIntervalMutationVariables + >(SCAN_INTERVAL_MUTATION) - const onScanIntervalCheckboxChange = checked => { + const onScanIntervalCheckboxChange = (checked: boolean) => { setEnablePeriodicScanner(checked) - onScanIntervalUpdate(checked ? scanInterval : { value: 0, unit: 'second' }) + onScanIntervalUpdate( + checked ? scanInterval : { value: 0, unit: TimeUnit.Second } + ) } - const onScanIntervalUpdate = scanInterval => { + const onScanIntervalUpdate = (scanInterval: TimeValue) => { const seconds = convertToSeconds(scanInterval) if (scanIntervalServerValue.current != seconds) { @@ -131,31 +154,37 @@ const PeriodicScanner = () => { } } - const scanIntervalUnits = [ + type scanIntervalUnitType = { + key: TimeUnit + text: string + value: TimeUnit + } + + const scanIntervalUnits: scanIntervalUnitType[] = [ { - key: 'second', + key: TimeUnit.Second, text: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'), - value: 'second', + value: TimeUnit.Second, }, { - key: 'minute', + key: TimeUnit.Minute, text: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'), - value: 'minute', + value: TimeUnit.Minute, }, { - key: 'hour', + key: TimeUnit.Hour, text: t('settings.periodic_scanner.interval_unit.hour', 'Hour'), - value: 'hour', + value: TimeUnit.Hour, }, { - key: 'day', + key: TimeUnit.Day, text: t('settings.periodic_scanner.interval_unit.days', 'Days'), - value: 'day', + value: TimeUnit.Day, }, { - key: 'month', + key: TimeUnit.Month, text: t('settings.periodic_scanner.interval_unit.months', 'Months'), - value: 'month', + value: TimeUnit.Month, }, ] @@ -171,7 +200,9 @@ const PeriodicScanner = () => { )} disabled={scanIntervalQuery.loading} checked={enablePeriodicScanner} - onChange={(_, { checked }) => onScanIntervalCheckboxChange(checked)} + onChange={(_, { checked }) => + onScanIntervalCheckboxChange(checked || false) + } /> @@ -195,9 +226,9 @@ const PeriodicScanner = () => { label={ { - const newScanInterval = { + const newScanInterval: TimeValue = { ...scanInterval, - unit: value, + unit: value as TimeUnit, } setScanInterval(newScanInterval) @@ -208,7 +239,7 @@ const PeriodicScanner = () => { /> } onBlur={() => onScanIntervalUpdate(scanInterval)} - onKeyDown={({ key }) => + onKeyDown={({ key }: KeyboardEvent) => key == 'Enter' && onScanIntervalUpdate(scanInterval) } loading={scanIntervalQuery.loading} @@ -219,7 +250,7 @@ const PeriodicScanner = () => { onChange={(_, { value }) => { setScanInterval(x => ({ ...x, - value, + value: parseInt(value), })) }} /> diff --git a/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.js b/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx similarity index 68% rename from ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.js rename to ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx index 9d736d7..3d6fc9f 100644 --- a/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.js +++ b/ui/src/Pages/SettingsPage/ScannerConcurrentWorkers.tsx @@ -3,6 +3,11 @@ import { useQuery, useMutation, gql } from '@apollo/client' import { Input, Loader } from 'semantic-ui-react' import { InputLabelTitle, InputLabelDescription } from './SettingsPage' import { useTranslation } from 'react-i18next' +import { concurrentWorkersQuery } from './__generated__/concurrentWorkersQuery' +import { + setConcurrentWorkers, + setConcurrentWorkersVariables, +} from './__generated__/setConcurrentWorkers' const CONCURRENT_WORKERS_QUERY = gql` query concurrentWorkersQuery { @@ -21,21 +26,25 @@ const SET_CONCURRENT_WORKERS_MUTATION = gql` const ScannerConcurrentWorkers = () => { const { t } = useTranslation() - const workerAmountQuery = useQuery(CONCURRENT_WORKERS_QUERY, { - onCompleted(data) { - setWorkerAmount(data.siteInfo.concurrentWorkers) - workerAmountServerValue.current = data.siteInfo.concurrentWorkers - }, - }) - - const [setWorkersMutation, workersMutationData] = useMutation( - SET_CONCURRENT_WORKERS_MUTATION + const workerAmountQuery = useQuery( + CONCURRENT_WORKERS_QUERY, + { + onCompleted(data) { + setWorkerAmount(data.siteInfo.concurrentWorkers) + workerAmountServerValue.current = data.siteInfo.concurrentWorkers + }, + } ) - const workerAmountServerValue = useRef(null) - const [workerAmount, setWorkerAmount] = useState('') + const [setWorkersMutation, workersMutationData] = useMutation< + setConcurrentWorkers, + setConcurrentWorkersVariables + >(SET_CONCURRENT_WORKERS_MUTATION) - const updateWorkerAmount = workerAmount => { + const workerAmountServerValue = useRef(null) + const [workerAmount, setWorkerAmount] = useState(0) + + const updateWorkerAmount = (workerAmount: number) => { if (workerAmountServerValue.current != workerAmount) { workerAmountServerValue.current = workerAmount setWorkersMutation({ @@ -67,10 +76,10 @@ const ScannerConcurrentWorkers = () => { id="scanner_concurrent_workers_field" value={workerAmount} onChange={(_, { value }) => { - setWorkerAmount(value) + setWorkerAmount(parseInt(value)) }} onBlur={() => updateWorkerAmount(workerAmount)} - onKeyDown={({ key }) => + onKeyDown={({ key }: KeyboardEvent) => key == 'Enter' && updateWorkerAmount(workerAmount) } /> diff --git a/ui/src/Pages/SettingsPage/ScannerSection.js b/ui/src/Pages/SettingsPage/ScannerSection.tsx similarity index 89% rename from ui/src/Pages/SettingsPage/ScannerSection.js rename to ui/src/Pages/SettingsPage/ScannerSection.tsx index ce14420..77070c7 100644 --- a/ui/src/Pages/SettingsPage/ScannerSection.js +++ b/ui/src/Pages/SettingsPage/ScannerSection.tsx @@ -5,6 +5,7 @@ import PeriodicScanner from './PeriodicScanner' import ScannerConcurrentWorkers from './ScannerConcurrentWorkers' import { SectionTitle, InputLabelDescription } from './SettingsPage' import { useTranslation } from 'react-i18next' +import { scanAllMutation } from './__generated__/scanAllMutation' const SCAN_MUTATION = gql` mutation scanAllMutation { @@ -17,7 +18,7 @@ const SCAN_MUTATION = gql` const ScannerSection = () => { const { t } = useTranslation() - const [startScanner, { called }] = useMutation(SCAN_MUTATION) + const [startScanner, { called }] = useMutation(SCAN_MUTATION) return (
diff --git a/ui/src/Pages/SettingsPage/SettingsPage.tsx b/ui/src/Pages/SettingsPage/SettingsPage.tsx index b9d0269..c8b1dbf 100644 --- a/ui/src/Pages/SettingsPage/SettingsPage.tsx +++ b/ui/src/Pages/SettingsPage/SettingsPage.tsx @@ -10,7 +10,7 @@ import ScannerSection from './ScannerSection' import UserPreferences from './UserPreferences' import UsersTable from './Users/UsersTable' -export const SectionTitle = styled.h2<{ nospace: boolean }>` +export const SectionTitle = styled.h2<{ nospace?: boolean }>` margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important; padding-bottom: 0.3em; border-bottom: 1px solid #ddd; diff --git a/ui/src/Pages/SettingsPage/UserPreferences.tsx b/ui/src/Pages/SettingsPage/UserPreferences.tsx index 3149286..c5bf833 100644 --- a/ui/src/Pages/SettingsPage/UserPreferences.tsx +++ b/ui/src/Pages/SettingsPage/UserPreferences.tsx @@ -5,7 +5,11 @@ import { useTranslation } from 'react-i18next' import { Dropdown } from 'semantic-ui-react' import styled from 'styled-components' import { LanguageTranslation } from '../../../__generated__/globalTypes' -import { SectionTitle } from './SettingsPage' +import { + InputLabelDescription, + InputLabelTitle, + SectionTitle, +} from './SettingsPage' import { changeUserPreferences, changeUserPreferencesVariables, @@ -13,8 +17,8 @@ import { import { myUserPreferences } from './__generated__/myUserPreferences' const languagePreferences = [ - { key: 1, text: 'English', value: LanguageTranslation.English }, - { key: 2, text: 'Dansk', value: LanguageTranslation.Danish }, + { key: 1, text: 'English', flag: 'uk', value: LanguageTranslation.English }, + { key: 2, text: 'Dansk', flag: 'dk', value: LanguageTranslation.Danish }, ] const CHANGE_USER_PREFERENCES = gql` @@ -58,7 +62,22 @@ const UserPreferences = () => { {t('settings.user_preferences.title', 'User preferences')} + { }) }} selection + search value={data?.myUserPreferences.language || undefined} loading={loadingPrefs} disabled={loadingPrefs} diff --git a/ui/src/Pages/SettingsPage/Users/AddUserRow.js b/ui/src/Pages/SettingsPage/Users/AddUserRow.tsx similarity index 86% rename from ui/src/Pages/SettingsPage/Users/AddUserRow.js rename to ui/src/Pages/SettingsPage/Users/AddUserRow.tsx index 831239f..efb7638 100644 --- a/ui/src/Pages/SettingsPage/Users/AddUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/AddUserRow.tsx @@ -1,10 +1,9 @@ import { gql, useMutation } from '@apollo/client' -import PropTypes from 'prop-types' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { Button, Checkbox, Input, Table } from 'semantic-ui-react' -const createUserMutation = gql` +const CREATE_USER_MUTATION = gql` mutation createUser($username: String!, $admin: Boolean!) { createUser(username: $username, admin: $admin) { id @@ -15,7 +14,7 @@ const createUserMutation = gql` } ` -export const userAddRootPathMutation = gql` +export const USER_ADD_ROOT_PATH_MUTATION = gql` mutation userAddRootPath($id: ID!, $rootPath: String!) { userAddRootPath(id: $id, rootPath: $rootPath) { id @@ -30,12 +29,18 @@ const initialState = { userAdded: false, } -const AddUserRow = ({ setShow, show, onUserAdded }) => { +type AddUserRowProps = { + setShow: React.Dispatch> + show: boolean + onUserAdded(): void +} + +const AddUserRow = ({ setShow, show, onUserAdded }: AddUserRowProps) => { const { t } = useTranslation() const [state, setState] = useState(initialState) const [addRootPath, { loading: addRootPathLoading }] = useMutation( - userAddRootPathMutation, + USER_ADD_ROOT_PATH_MUTATION, { onCompleted: () => { setState(initialState) @@ -49,7 +54,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { ) const [createUser, { loading: createUserLoading }] = useMutation( - createUserMutation, + CREATE_USER_MUTATION, { onCompleted: ({ createUser: { id } }) => { if (state.rootPath) { @@ -68,7 +73,10 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { const loading = addRootPathLoading || createUserLoading - function updateInput(event, key) { + function updateInput( + event: React.ChangeEvent, + key: string + ) { setState({ ...state, [key]: event.target.value, @@ -105,7 +113,7 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { onChange={(e, data) => { setState({ ...state, - admin: data.checked, + admin: data.checked || false, }) }} /> @@ -137,10 +145,4 @@ const AddUserRow = ({ setShow, show, onUserAdded }) => { ) } -AddUserRow.propTypes = { - setShow: PropTypes.func.isRequired, - show: PropTypes.bool.isRequired, - onUserAdded: PropTypes.func.isRequired, -} - export default AddUserRow diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRow.js b/ui/src/Pages/SettingsPage/Users/EditUserRow.tsx similarity index 87% rename from ui/src/Pages/SettingsPage/Users/EditUserRow.js rename to ui/src/Pages/SettingsPage/Users/EditUserRow.tsx index 3c5d9f2..9f03050 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRow.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { Button, Checkbox, Input, Table } from 'semantic-ui-react' import { EditRootPaths } from './EditUserRowRootPaths' -import { UserRowProps } from './UserRow' +import { UserRowProps, UserRowChildProps } from './UserRow' const EditUserRow = ({ user, @@ -10,9 +10,13 @@ const EditUserRow = ({ setState, updateUser, updateUserLoading, -}) => { +}: UserRowChildProps) => { const { t } = useTranslation() - function updateInput(event, key) { + + function updateInput( + event: React.ChangeEvent, + key: string + ) { setState(state => ({ ...state, [key]: event.target.value, @@ -39,7 +43,7 @@ const EditUserRow = ({ onChange={(_, data) => { setState(state => ({ ...state, - admin: data.checked, + admin: data.checked || false, })) }} /> @@ -50,6 +54,7 @@ const EditUserRow = ({ negative onClick={() => setState(state => ({ + ...state, ...state.oldState, })) } diff --git a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx similarity index 65% rename from ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js rename to ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx index ddc5c00..1dacddb 100644 --- a/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.js +++ b/ui/src/Pages/SettingsPage/Users/EditUserRowRootPaths.tsx @@ -1,13 +1,21 @@ -import PropTypes from 'prop-types' import React, { useState } from 'react' import { gql, useMutation } from '@apollo/client' import { Button, Icon, Input } from 'semantic-ui-react' import styled from 'styled-components' import { USERS_QUERY } from './UsersTable' import { useTranslation } from 'react-i18next' -import { userAddRootPathMutation } from './AddUserRow' +import { USER_ADD_ROOT_PATH_MUTATION } from './AddUserRow' +import { + userRemoveAlbumPathMutation, + userRemoveAlbumPathMutationVariables, +} from './__generated__/userRemoveAlbumPathMutation' +import { + settingsUsersQuery_user, + settingsUsersQuery_user_rootAlbums, +} from './__generated__/settingsUsersQuery' +import { userAddRootPath } from './__generated__/userAddRootPath' -const userRemoveAlbumPathMutation = gql` +const USER_REMOVE_ALBUM_PATH_MUTATION = gql` mutation userRemoveAlbumPathMutation($userId: ID!, $albumId: ID!) { userRemoveRootAlbum(userId: $userId, albumId: $albumId) { id @@ -21,18 +29,23 @@ const RootPathListItem = styled.li` align-items: center; ` -const EditRootPath = ({ album, user }) => { +type EditRootPathProps = { + album: settingsUsersQuery_user_rootAlbums + user: settingsUsersQuery_user +} + +const EditRootPath = ({ album, user }: EditRootPathProps) => { const { t } = useTranslation() - const [removeAlbumPath, { loading }] = useMutation( + const [removeAlbumPath, { loading }] = useMutation< userRemoveAlbumPathMutation, - { - refetchQueries: [ - { - query: USERS_QUERY, - }, - ], - } - ) + userRemoveAlbumPathMutationVariables + >(USER_REMOVE_ALBUM_PATH_MUTATION, { + refetchQueries: [ + { + query: USERS_QUERY, + }, + ], + }) return ( @@ -56,33 +69,37 @@ const EditRootPath = ({ album, user }) => { ) } -EditRootPath.propTypes = { - album: PropTypes.object.isRequired, - user: PropTypes.object.isRequired, -} - const NewRootPathInput = styled(Input)` width: 100%; margin-top: 24px; ` -const EditNewRootPath = ({ userID }) => { +type EditNewRootPathProps = { + userID: string +} + +const EditNewRootPath = ({ userID }: EditNewRootPathProps) => { const { t } = useTranslation() const [value, setValue] = useState('') - const [addRootPath, { loading }] = useMutation(userAddRootPathMutation, { - refetchQueries: [ - { - query: USERS_QUERY, - }, - ], - }) + const [addRootPath, { loading }] = useMutation( + USER_ADD_ROOT_PATH_MUTATION, + { + refetchQueries: [ + { + query: USERS_QUERY, + }, + ], + } + ) return (
  • setValue(e.target.value)} + onChange={(e: React.ChangeEvent) => + setValue(e.target.value) + } disabled={loading} action={{ positive: true, @@ -103,17 +120,17 @@ const EditNewRootPath = ({ userID }) => { ) } -EditNewRootPath.propTypes = { - userID: PropTypes.string.isRequired, -} - const RootPathList = styled.ul` margin: 0; padding: 0; list-style: none; ` -export const EditRootPaths = ({ user }) => { +type EditRootPathsProps = { + user: settingsUsersQuery_user +} + +export const EditRootPaths = ({ user }: EditRootPathsProps) => { const editRows = user.rootAlbums.map(album => ( )) @@ -125,7 +142,3 @@ export const EditRootPaths = ({ user }) => { ) } - -EditRootPaths.propTypes = { - user: PropTypes.object.isRequired, -} diff --git a/ui/src/Pages/SettingsPage/Users/UserChangePassword.js b/ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx similarity index 82% rename from ui/src/Pages/SettingsPage/Users/UserChangePassword.js rename to ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx index d7a537e..d807bd7 100644 --- a/ui/src/Pages/SettingsPage/Users/UserChangePassword.js +++ b/ui/src/Pages/SettingsPage/Users/UserChangePassword.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react' -import PropTypes from 'prop-types' import { gql, useMutation } from '@apollo/client' -import { Button, Form, Input, Modal } from 'semantic-ui-react' +import { Button, Form, Input, Modal, ModalProps } from 'semantic-ui-react' import { Trans, useTranslation } from 'react-i18next' +import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery' const changeUserPasswordMutation = gql` mutation changeUserPassword($userId: ID!, $password: String!) { @@ -12,7 +12,18 @@ const changeUserPasswordMutation = gql` } ` -const ChangePasswordModal = ({ onClose, user, ...props }) => { +interface ChangePasswordModalProps extends ModalProps { + onClose(): void + open: boolean + user: settingsUsersQuery_user +} + +const ChangePasswordModal = ({ + onClose, + user, + open, + ...props +}: ChangePasswordModalProps) => { const { t } = useTranslation() const [passwordInput, setPasswordInput] = useState('') @@ -23,7 +34,7 @@ const ChangePasswordModal = ({ onClose, user, ...props }) => { }) return ( - + {t('settings.users.password_reset.title', 'Change password')} @@ -71,9 +82,4 @@ const ChangePasswordModal = ({ onClose, user, ...props }) => { ) } -ChangePasswordModal.propTypes = { - onClose: PropTypes.func, - user: PropTypes.object.isRequired, -} - export default ChangePasswordModal diff --git a/ui/src/Pages/SettingsPage/Users/UserRow.js b/ui/src/Pages/SettingsPage/Users/UserRow.js deleted file mode 100644 index 05728b9..0000000 --- a/ui/src/Pages/SettingsPage/Users/UserRow.js +++ /dev/null @@ -1,111 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useState } from 'react' -import { gql, useMutation } from '@apollo/client' -import EditUserRow from './EditUserRow' -import ViewUserRow from './ViewUserRow' - -const updateUserMutation = gql` - mutation updateUser($id: ID!, $username: String, $admin: Boolean) { - updateUser(id: $id, username: $username, admin: $admin) { - id - username - admin - } - } -` - -const deleteUserMutation = gql` - mutation deleteUser($id: ID!) { - deleteUser(id: $id) { - id - username - } - } -` - -const scanUserMutation = gql` - mutation scanUser($userId: ID!) { - scanUser(userId: $userId) { - success - } - } -` - -const UserRow = ({ user, refetchUsers }) => { - const [state, setState] = useState({ - ...user, - editing: false, - newRootPath: '', - }) - - const [showConfirmDelete, setConfirmDelete] = useState(false) - const [showChangePassword, setChangePassword] = useState(false) - - const [updateUser, { loading: updateUserLoading }] = useMutation( - updateUserMutation, - { - onCompleted: data => { - setState({ - ...data.updateUser, - editing: false, - }) - refetchUsers() - }, - } - ) - - const [deleteUser] = useMutation(deleteUserMutation, { - onCompleted: () => { - refetchUsers() - }, - }) - - const [scanUser, { called: scanUserCalled }] = useMutation(scanUserMutation, { - onCompleted: () => { - refetchUsers() - }, - }) - - const props = { - user, - state, - setState, - scanUser, - updateUser, - updateUserLoading, - deleteUser, - setChangePassword, - setConfirmDelete, - scanUserCalled, - showChangePassword, - showConfirmDelete, - } - - if (state.editing) { - return - } - - return -} - -UserRow.propTypes = { - user: PropTypes.object.isRequired, - refetchUsers: PropTypes.func.isRequired, -} - -export const UserRowProps = { - user: PropTypes.object.isRequired, - state: PropTypes.object.isRequired, - setState: PropTypes.func.isRequired, - scanUser: PropTypes.func.isRequired, - updateUser: PropTypes.func.isRequired, - updateUserLoading: PropTypes.bool.isRequired, - deleteUser: PropTypes.func.isRequired, - setChangePassword: PropTypes.func.isRequired, - setConfirmDelete: PropTypes.func.isRequired, - scanUserCalled: PropTypes.func.isRequired, - showChangePassword: PropTypes.func.isRequired, - showConfirmDelete: PropTypes.func.isRequired, -} - -export default UserRow diff --git a/ui/src/Pages/SettingsPage/Users/UserRow.tsx b/ui/src/Pages/SettingsPage/Users/UserRow.tsx new file mode 100644 index 0000000..23243c3 --- /dev/null +++ b/ui/src/Pages/SettingsPage/Users/UserRow.tsx @@ -0,0 +1,153 @@ +import PropTypes from 'prop-types' +import React, { useState } from 'react' +import { + FetchResult, + gql, + MutationFunctionOptions, + useMutation, +} from '@apollo/client' +import EditUserRow from './EditUserRow' +import ViewUserRow from './ViewUserRow' +import { settingsUsersQuery_user } from './__generated__/settingsUsersQuery' +import { scanUser, scanUserVariables } from './__generated__/scanUser' +import { updateUser, updateUserVariables } from './__generated__/updateUser' +import { deleteUser, deleteUserVariables } from './__generated__/deleteUser' + +const updateUserMutation = gql` + mutation updateUser($id: ID!, $username: String, $admin: Boolean) { + updateUser(id: $id, username: $username, admin: $admin) { + id + username + admin + } + } +` + +const deleteUserMutation = gql` + mutation deleteUser($id: ID!) { + deleteUser(id: $id) { + id + username + } + } +` + +const scanUserMutation = gql` + mutation scanUser($userId: ID!) { + scanUser(userId: $userId) { + success + } + } +` + +export const UserRowProps = { + user: PropTypes.object.isRequired, + state: PropTypes.object.isRequired, + setState: PropTypes.func.isRequired, + scanUser: PropTypes.func.isRequired, + updateUser: PropTypes.func.isRequired, + updateUserLoading: PropTypes.bool.isRequired, + deleteUser: PropTypes.func.isRequired, + setChangePassword: PropTypes.func.isRequired, + setConfirmDelete: PropTypes.func.isRequired, + scanUserCalled: PropTypes.func.isRequired, + showChangePassword: PropTypes.func.isRequired, + showConfirmDelete: PropTypes.func.isRequired, +} + +interface UserRowState extends settingsUsersQuery_user { + editing: boolean + newRootPath: string + oldState?: Omit +} + +type ApolloMutationFn = ( + options?: MutationFunctionOptions + // eslint-disable-next-line @typescript-eslint/no-explicit-any +) => Promise> + +export type UserRowChildProps = { + user: settingsUsersQuery_user + state: UserRowState + setState: React.Dispatch> + scanUser: ApolloMutationFn + updateUser: ApolloMutationFn + updateUserLoading: boolean + deleteUser: ApolloMutationFn + setChangePassword: React.Dispatch> + setConfirmDelete: React.Dispatch> + scanUserCalled: boolean + showChangePassword: boolean + showConfirmDelete: boolean +} + +type UserRowProps = { + user: settingsUsersQuery_user + refetchUsers(): void +} + +const UserRow = ({ user, refetchUsers }: UserRowProps) => { + const [state, setState] = useState({ + ...user, + editing: false, + newRootPath: '', + }) + + const [showConfirmDelete, setConfirmDelete] = useState(false) + const [showChangePassword, setChangePassword] = useState(false) + + const [updateUser, { loading: updateUserLoading }] = useMutation< + updateUser, + updateUserVariables + >(updateUserMutation, { + onCompleted: data => { + setState(state => ({ + ...state, + ...data.updateUser, + editing: false, + })) + refetchUsers() + }, + }) + + const [deleteUser] = useMutation( + deleteUserMutation, + { + onCompleted: () => { + refetchUsers() + }, + } + ) + + const [scanUser, { called: scanUserCalled }] = useMutation< + scanUser, + scanUserVariables + >(scanUserMutation, { + onCompleted: () => { + refetchUsers() + }, + }) + + const props: UserRowChildProps = { + user, + state, + setState, + scanUser, + updateUser, + updateUserLoading, + deleteUser, + setChangePassword, + setConfirmDelete, + scanUserCalled, + showChangePassword, + showConfirmDelete, + } + + if (state.editing) { + return + } + + return +} + +export default UserRow diff --git a/ui/src/Pages/SettingsPage/Users/UsersTable.js b/ui/src/Pages/SettingsPage/Users/UsersTable.tsx similarity index 91% rename from ui/src/Pages/SettingsPage/Users/UsersTable.js rename to ui/src/Pages/SettingsPage/Users/UsersTable.tsx index 4d7f21f..b905ecd 100644 --- a/ui/src/Pages/SettingsPage/Users/UsersTable.js +++ b/ui/src/Pages/SettingsPage/Users/UsersTable.tsx @@ -6,13 +6,13 @@ import UserRow from './UserRow' import AddUserRow from './AddUserRow' import { SectionTitle } from '../SettingsPage' import { useTranslation } from 'react-i18next' +import { settingsUsersQuery } from './__generated__/settingsUsersQuery' export const USERS_QUERY = gql` query settingsUsersQuery { user { id username - # rootPath admin rootAlbums { id @@ -26,14 +26,16 @@ const UsersTable = () => { const { t } = useTranslation() const [showAddUser, setShowAddUser] = useState(false) - const { loading, error, data, refetch } = useQuery(USERS_QUERY) + const { loading, error, data, refetch } = useQuery( + USERS_QUERY + ) if (error) { return
    {`Users table error: ${error.message}`}
    } - let userRows = [] - if (data && data.user) { + let userRows: JSX.Element[] = [] + if (data?.user) { userRows = data.user.map(user => ( )) diff --git a/ui/src/Pages/SettingsPage/Users/ViewUserRow.js b/ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx similarity index 92% rename from ui/src/Pages/SettingsPage/Users/ViewUserRow.js rename to ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx index b2d6ef3..f4b5b7e 100644 --- a/ui/src/Pages/SettingsPage/Users/ViewUserRow.js +++ b/ui/src/Pages/SettingsPage/Users/ViewUserRow.tsx @@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next' import { Button, Icon, Table, Modal } from 'semantic-ui-react' import styled from 'styled-components' import ChangePasswordModal from './UserChangePassword' -import { UserRowProps } from './UserRow' +import { UserRowChildProps, UserRowProps } from './UserRow' const PathList = styled.ul` margin: 0; @@ -22,7 +22,7 @@ const ViewUserRow = ({ scanUserCalled, showChangePassword, showConfirmDelete, -}) => { +}: UserRowChildProps) => { const { t } = useTranslation() const paths = ( @@ -43,7 +43,11 @@ const ViewUserRow = ({