From 264ee54e7ff38eb0a54ef2b1d4820fdd4dde25ec Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 12 Apr 2021 00:14:27 +0200 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 08/11] 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 09/11] 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 = ({