From 52545c8901f9f49e31a4d59d762845f498cd666e Mon Sep 17 00:00:00 2001 From: Eugen Ciur Date: Wed, 9 Oct 2024 08:06:39 +0200 Subject: [PATCH] add calendar widget to custom field of type date --- papermerge/core/constants.py | 46 +++--- papermerge/core/db/doc.py | 5 +- tests/core/models/test_document.py | 18 ++- ui2/package.json | 16 ++- ui2/src/app/App.tsx | 22 +-- .../DocumentDetails/CustomFields.tsx | 9 +- .../customFields/CustomFieldDate.tsx | 61 ++++++++ .../components/customFields/index.tsx | 3 + ui2/yarn.lock | 132 ++++++++++++------ 9 files changed, 217 insertions(+), 95 deletions(-) create mode 100644 ui2/src/features/document/components/customFields/CustomFieldDate.tsx create mode 100644 ui2/src/features/document/components/customFields/index.tsx diff --git a/papermerge/core/constants.py b/papermerge/core/constants.py index cdffb95da..c2668812b 100644 --- a/papermerge/core/constants.py +++ b/papermerge/core/constants.py @@ -1,24 +1,26 @@ -INBOX_TITLE = '.inbox' -HOME_TITLE = '.home' -CTYPE_FOLDER = 'folder' -CTYPE_DOCUMENT = 'document' +INBOX_TITLE = ".inbox" +HOME_TITLE = ".home" +CTYPE_FOLDER = "folder" +CTYPE_DOCUMENT = "document" DEFAULT_THUMBNAIL_SIZE = 100 # 100 pixels wide DEFAULT_PAGE_SIZE = 900 # 900 pixels wide -JPG = 'jpg' -PAGES = 'pages' -THUMBNAILS = 'thumbnails' -DOCVERS = 'docvers' -OCR = 'ocr' -DEFAULT_TAG_BG_COLOR = '#c41fff' -DEFAULT_TAG_FG_COLOR = '#ffffff' -INDEX_ADD_NODE = 'index_add_node' -INDEX_ADD_DOCS = 'index_add_docs' -INDEX_ADD_PAGES = 'index_add_pages' -INDEX_REMOVE_NODE = 'index_remove_node' -INDEX_UPDATE = 'index_update' -S3_WORKER_ADD_DOC_VER = 's3_worker_add_doc_vers' -S3_WORKER_REMOVE_DOC_VER = 's3_worker_remove_doc_vers' -S3_WORKER_REMOVE_DOC_THUMBNAIL = 's3_worker_remove_doc_thumbnail' -S3_WORKER_REMOVE_PAGE_THUMBNAIL = 's3_worker_remove_page_thumbnail' -S3_WORKER_GENERATE_PREVIEW = 's3_worker_generate_preview' -WORKER_OCR_DOCUMENT = 'worker_ocr_document' +JPG = "jpg" +PAGES = "pages" +THUMBNAILS = "thumbnails" +DOCVERS = "docvers" +OCR = "ocr" +DEFAULT_TAG_BG_COLOR = "#c41fff" +DEFAULT_TAG_FG_COLOR = "#ffffff" +INDEX_ADD_NODE = "index_add_node" +INDEX_ADD_DOCS = "index_add_docs" +INDEX_ADD_PAGES = "index_add_pages" +INDEX_REMOVE_NODE = "index_remove_node" +INDEX_UPDATE = "index_update" +S3_WORKER_ADD_DOC_VER = "s3_worker_add_doc_vers" +S3_WORKER_REMOVE_DOC_VER = "s3_worker_remove_doc_vers" +S3_WORKER_REMOVE_DOC_THUMBNAIL = "s3_worker_remove_doc_thumbnail" +S3_WORKER_REMOVE_PAGE_THUMBNAIL = "s3_worker_remove_page_thumbnail" +S3_WORKER_GENERATE_PREVIEW = "s3_worker_generate_preview" +WORKER_OCR_DOCUMENT = "worker_ocr_document" +# incoming (from user) date format +INCOMING_DATE_FORMAT = "%Y-%m-%d" diff --git a/papermerge/core/db/doc.py b/papermerge/core/db/doc.py index 02b8a922e..50cdbc8d1 100644 --- a/papermerge/core/db/doc.py +++ b/papermerge/core/db/doc.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import Session from papermerge.core import schemas +from papermerge.core.constants import INCOMING_DATE_FORMAT from papermerge.core.db.models import ( ColoredTag, CustomField, @@ -135,7 +136,7 @@ def update_document_custom_field_values( if attr_name: if attr_name == "date": _dic[f"value_{attr_name}"] = datetime.strptime( - incoming_cf.value, "%d.%m.%Y" + incoming_cf.value, INCOMING_DATE_FORMAT ) else: _dic[f"value_{attr_name}"] = incoming_cf.value @@ -215,7 +216,7 @@ def add_document_custom_field_values( value = "" if attr_name: if attr_name == "date": - value = datetime.strptime(incoming_cf.value, "%d.%m.%Y") + value = datetime.strptime(incoming_cf.value, INCOMING_DATE_FORMAT) else: value = incoming_cf.value _dic[f"value_{attr_name}"] = value diff --git a/tests/core/models/test_document.py b/tests/core/models/test_document.py index 4a23b7958..21d8afe71 100644 --- a/tests/core/models/test_document.py +++ b/tests/core/models/test_document.py @@ -269,7 +269,7 @@ def test_document_add_custom_field_value_of_type_date( document_type_with_one_date_cf: schemas.DocumentType, ): """ - Custom field of type `date` is set to string "28.10.2024" + Custom field of type `date` is set to string "2024-10-28" """ total_cfv_after = db.get_document_custom_field_values( db_session, id=document.id, user_id=document.user.id @@ -280,7 +280,9 @@ def test_document_add_custom_field_value_of_type_date( cf_add = { "document_type_id": dtype.id, "custom_fields": [ - {"custom_field_id": dtype.custom_fields[0].id, "value": "28.10.2024"}, + # date is expected to be in: + # papermerge.core.constants.INCOMING_DATE_FORMAT + {"custom_field_id": dtype.custom_fields[0].id, "value": "2024-10-28"}, ], } custom_fields_add = schemas.DocumentCustomFieldsAdd(**cf_add) @@ -311,7 +313,9 @@ def test_document_update_same_custom_field_value_multiple_times1( cf_add = { "document_type_id": dtype.id, "custom_fields": [ - {"custom_field_id": dtype.custom_fields[0].id, "value": "28.10.2024"}, + # date is expected to be in: + # papermerge.core.constants.INCOMING_DATE_FORMAT + {"custom_field_id": dtype.custom_fields[0].id, "value": "2024-10-28"}, ], } custom_fields_add = schemas.DocumentCustomFieldsAdd(**cf_add) @@ -329,7 +333,9 @@ def test_document_update_same_custom_field_value_multiple_times1( ) assert len(total_cfv_after1) == 1 - for value in ["29.10.2024", "30.10.2024"]: + # date is expected to be in: + # papermerge.core.constants.INCOMING_DATE_FORMAT + for value in ["2024-10-29", "2024-10-30"]: # updating same custom field multiple times should not raise # exceptions cf_update = { @@ -372,7 +378,7 @@ def test_document_update_same_custom_field_value_multiple_times2( cf_add = { "document_type_id": dtype.id, "custom_fields": [ - {"custom_field_id": dtype.custom_fields[0].id, "value": "28.10.2024"}, + {"custom_field_id": dtype.custom_fields[0].id, "value": "2024-10-28"}, ], } custom_fields_add = schemas.DocumentCustomFieldsAdd(**cf_add) @@ -395,7 +401,7 @@ def test_document_update_same_custom_field_value_multiple_times2( cf_update = { "document_type_id": dtype.id, "custom_fields": [ - {"custom_field_value_id": total_cfv_after1[0].id, "value": "29.10.2024"}, + {"custom_field_value_id": total_cfv_after1[0].id, "value": "2024-10-29"}, ], } custom_fields_update = schemas.DocumentCustomFieldsUpdate(**cf_update) diff --git a/ui2/package.json b/ui2/package.json index 5964ef575..fb681a0f6 100644 --- a/ui2/package.json +++ b/ui2/package.json @@ -12,18 +12,20 @@ "preview": "vite preview" }, "dependencies": { - "@mantine/core": "^7.12.2", - "@mantine/form": "^7.12.2", - "@mantine/hooks": "^7.12.2", - "@mantine/notifications": "^7.12.2", - "@reduxjs/toolkit": "^2.2.5", - "@tabler/icons-react": "^3.6.0", + "@mantine/core": "^7.13.2", + "@mantine/dates": "^7.13.2", + "@mantine/form": "^7.13.2", + "@mantine/hooks": "^7.13.2", + "@mantine/notifications": "^7.13.2", + "@reduxjs/toolkit": "^2.2.8", + "@tabler/icons-react": "^3.19.0", "axios": "^1.7.2", + "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.1.2", - "react-router": "^6.23.1", + "react-router": "^6.26.2", "react-router-dom": "^6.23.1" }, "devDependencies": { diff --git a/ui2/src/app/App.tsx b/ui2/src/app/App.tsx index 3ad43539a..6c77223ca 100644 --- a/ui2/src/app/App.tsx +++ b/ui2/src/app/App.tsx @@ -1,23 +1,23 @@ -import {useLocation} from "react-router-dom" -import {useEffect, useRef} from "react" -import "@mantine/core/styles.css" import {AppShell} from "@mantine/core" +import "@mantine/core/styles.css" +import "@mantine/dates/styles.css" import {useViewportSize} from "@mantine/hooks" -import {Outlet, useNavigate} from "react-router-dom" -import {useSelector, useDispatch} from "react-redux" +import {useEffect, useRef} from "react" +import {useDispatch, useSelector} from "react-redux" +import {Outlet, useLocation, useNavigate} from "react-router-dom" -import NavBar from "@/components/NavBar" import Header from "@/components/Header/Header" +import NavBar from "@/components/NavBar" +import {updateOutlet} from "@/features/ui/uiSlice" import { + selectCurrentUser, selectCurrentUserError, - selectCurrentUserStatus, - selectCurrentUser + selectCurrentUserStatus } from "@/slices/currentUser" -import {updateOutlet} from "@/features/ui/uiSlice" -import "./App.css" -import {selectNavBarWidth} from "@/features/ui/uiSlice" import Uploader from "@/components/Uploader" +import {selectNavBarWidth} from "@/features/ui/uiSlice" +import "./App.css" function App() { const {height, width} = useViewportSize() diff --git a/ui2/src/features/document/components/DocumentDetails/CustomFields.tsx b/ui2/src/features/document/components/DocumentDetails/CustomFields.tsx index 54394ee35..15dae7a7a 100644 --- a/ui2/src/features/document/components/DocumentDetails/CustomFields.tsx +++ b/ui2/src/features/document/components/DocumentDetails/CustomFields.tsx @@ -3,6 +3,7 @@ import {useContext, useEffect, useState} from "react" import PanelContext from "@/contexts/PanelContext" import {useGetDocumentQuery} from "@/features/document/apiSlice" +import {CustomFieldDate} from "@/features/document/components/customFields" import {skipToken} from "@reduxjs/toolkit/query" import { @@ -143,7 +144,7 @@ export default function CustomFields() { } const onSave = async () => { - if (documentCustomFields && customFieldValues.length > 0) { + if (documentCustomFields && documentCustomFields.length > 0) { // document already has custom fields associated // we need to update existing custom field value const data = { @@ -164,7 +165,7 @@ export default function CustomFields() { body: { document_type_id: documentTypeID?.value!, custom_fields: customFieldValues.map(i => { - return {custom_field_id: i.field_id!, value: i.value} + return {custom_field_id: i.id, value: i.value} }) } } @@ -232,6 +233,10 @@ function GenericCustomField({ return } + if (customField.data_type == "date") { + return + } + return ( void + +interface Args { + customField: DocumentCustomFieldValue + onChange: onChangeType +} + +export default function CustomFieldDate({customField, onChange}: Args) { + const [value, setValue] = useState(null) + const icon = ( + + ) + + useEffect(() => { + if (customField.value && customField.value.length > 0) { + const parts = customField.value.split("-") + const year = Number(parts[0]) + const month = Number(parts[1]) - 1 + const day = Number(parts[2].substring(0, 2)) + + const date = new Date(year, month, day) + setValue(date) + } + }, []) + + const onLocalChange = (value: DateValue) => { + if (value) { + const d = dayjs(value) + const DATE_FORMAT = "YYYY-MM-DD" + const strValue = d.format(DATE_FORMAT) + onChange({customField, value: strValue}) + } + setValue(value) + } + + return ( + + ) +} diff --git a/ui2/src/features/document/components/customFields/index.tsx b/ui2/src/features/document/components/customFields/index.tsx new file mode 100644 index 000000000..14caf24b1 --- /dev/null +++ b/ui2/src/features/document/components/customFields/index.tsx @@ -0,0 +1,3 @@ +import CustomFieldDate from "./CustomFieldDate" + +export {CustomFieldDate} diff --git a/ui2/yarn.lock b/ui2/yarn.lock index dc3e16898..3ae7bc112 100644 --- a/ui2/yarn.lock +++ b/ui2/yarn.lock @@ -560,9 +560,9 @@ __metadata: languageName: node linkType: hard -"@mantine/core@npm:^7.12.2": - version: 7.12.2 - resolution: "@mantine/core@npm:7.12.2" +"@mantine/core@npm:^7.13.2": + version: 7.13.2 + resolution: "@mantine/core@npm:7.13.2" dependencies: "@floating-ui/react": "npm:^0.26.9" clsx: "npm:^2.1.1" @@ -571,55 +571,70 @@ __metadata: react-textarea-autosize: "npm:8.5.3" type-fest: "npm:^4.12.0" peerDependencies: - "@mantine/hooks": 7.12.2 + "@mantine/hooks": 7.13.2 react: ^18.2.0 react-dom: ^18.2.0 - checksum: 10c0/75643e8e6f33564e98b68259aedbc9ef543832e6466e93686b832587fcd187314d493ff4cfdb143acd3d21d835b4efe1bee1da853702ab9dbd6a9a758c2c1ce8 + checksum: 10c0/e5c41c73cea377a8c62683ab1abd24cc26394b68d0230f8df5b9925cd76dff52c6386a00f2c5f3221d7e9e82aeb988ca8e0f2aa042cc2378dea079e932a2290e languageName: node linkType: hard -"@mantine/form@npm:^7.12.2": - version: 7.12.2 - resolution: "@mantine/form@npm:7.12.2" +"@mantine/dates@npm:^7.13.2": + version: 7.13.2 + resolution: "@mantine/dates@npm:7.13.2" + dependencies: + clsx: "npm:^2.1.1" + peerDependencies: + "@mantine/core": 7.13.2 + "@mantine/hooks": 7.13.2 + dayjs: ">=1.0.0" + react: ^18.2.0 + react-dom: ^18.2.0 + checksum: 10c0/a48f01df14ff9608b105a157264f4fce1f56a7415d41a5ea709f6da95711dc39a0ec2a50893a4b369b05199f8f66711848864d515713d49f4b258a41c0677b54 + languageName: node + linkType: hard + +"@mantine/form@npm:^7.13.2": + version: 7.13.2 + resolution: "@mantine/form@npm:7.13.2" dependencies: fast-deep-equal: "npm:^3.1.3" klona: "npm:^2.0.6" peerDependencies: react: ^18.2.0 - checksum: 10c0/6c742b2d498fb94bb81085a53c8304eb2d6cc59b3f89be0f73857c0cb13eeb38eb4f385b549b14573ef80cf905c8174f2f98603470c11a60dfcfb8d15a44b08c + checksum: 10c0/9ee6cc3998a7ef15ce08ba72ea99d9a226ff1c8894ecdbb9b4d636b460d480e60ef2b91bfc6513ca4dc1815cebd446353864a3fd3f42bec79c2c1b0d603517da languageName: node linkType: hard -"@mantine/hooks@npm:^7.12.2": - version: 7.12.2 - resolution: "@mantine/hooks@npm:7.12.2" +"@mantine/hooks@npm:^7.13.2": + version: 7.13.2 + resolution: "@mantine/hooks@npm:7.13.2" peerDependencies: react: ^18.2.0 - checksum: 10c0/818674346619c2d30f53114a6b6da2b01cf6cf9ea1e262046964769d47c512fd75300be218651062df426822cfb38a0bc76d093798c8b4ddef66576c6fcf46e2 + checksum: 10c0/2d56c22e268689e0f25a2c3616af9e51aa3468338dccac5da03b18ecbce36424252ed7c8479da5861720763c714989f73d24bbfb1c3e0cc87647100480720268 languageName: node linkType: hard -"@mantine/notifications@npm:^7.12.2": - version: 7.12.2 - resolution: "@mantine/notifications@npm:7.12.2" +"@mantine/notifications@npm:^7.13.2": + version: 7.13.2 + resolution: "@mantine/notifications@npm:7.13.2" dependencies: - "@mantine/store": "npm:7.12.2" + "@mantine/store": "npm:7.13.2" react-transition-group: "npm:4.4.5" peerDependencies: - "@mantine/core": 7.12.2 - "@mantine/hooks": 7.12.2 + "@mantine/core": 7.13.2 + "@mantine/hooks": 7.13.2 react: ^18.2.0 react-dom: ^18.2.0 - checksum: 10c0/d35834741bc16cae89fc04cca009bd4eb5674e17d35e4594583c7b473259de2286533d0d820e6ba258aee6568aa9bf8e12316dedcdb0bf444361493f829926ba + checksum: 10c0/637ff28c36989b13e5abb6a94be67fd08444f9a47b3b66d8d834c1741f5219288c834e07d1bdc40aba0f9f9e7d24cb91627c56a9bfee9eedc698cc64688a96d5 languageName: node linkType: hard -"@mantine/store@npm:7.12.2": - version: 7.12.2 - resolution: "@mantine/store@npm:7.12.2" +"@mantine/store@npm:7.13.2": + version: 7.13.2 + resolution: "@mantine/store@npm:7.13.2" peerDependencies: react: ^18.2.0 - checksum: 10c0/a4f1aead30a0a8bc5deef7ea34d3e18e7d31de06fb0faaea1b3c74072c9c1fac81b08cd79723f9666f1424a3266b5628301c0580da9648bc602dfed455fb8e78 + checksum: 10c0/4c6f95e5aa06d794111372d5c513f8b0c30cd6e7421bbd1f68d71ac75becd7b6f99dba84be7543a2995ce46ca6e4293802fc2d0ec8304cd871740b0f404f1df8 languageName: node linkType: hard @@ -652,9 +667,9 @@ __metadata: languageName: node linkType: hard -"@reduxjs/toolkit@npm:^2.2.5": - version: 2.2.5 - resolution: "@reduxjs/toolkit@npm:2.2.5" +"@reduxjs/toolkit@npm:^2.2.8": + version: 2.2.8 + resolution: "@reduxjs/toolkit@npm:2.2.8" dependencies: immer: "npm:^10.0.3" redux: "npm:^5.0.1" @@ -668,7 +683,7 @@ __metadata: optional: true react-redux: optional: true - checksum: 10c0/be0593bf26852482fb8716b9248531466c6e8782a3114b823ae680fce90267d8c5512a3231cfecc30b17eff81a4604112772b49ad7ca6a3366ddd4f2a838e53c + checksum: 10c0/bf1356d71bfb82e5a181692c79c19b7bc19355260a9966f6562604c995f0cd0ce1154177ccd14095e8b319e73f64cfe86a4e46a83d24edba7876d4ae71fd5ae0 languageName: node linkType: hard @@ -679,6 +694,13 @@ __metadata: languageName: node linkType: hard +"@remix-run/router@npm:1.19.2": + version: 1.19.2 + resolution: "@remix-run/router@npm:1.19.2" + checksum: 10c0/ac7fc813350686705f2c29219e70e1e299d9a8e3b301e9e81f7e84f578c40c6462b590cf0d78863bac40dbc325b68c71ae070f4a1465793d1d1971b619618295 + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.18.0": version: 4.18.0 resolution: "@rollup/rollup-android-arm-eabi@npm:4.18.0" @@ -791,21 +813,21 @@ __metadata: languageName: node linkType: hard -"@tabler/icons-react@npm:^3.6.0": - version: 3.6.0 - resolution: "@tabler/icons-react@npm:3.6.0" +"@tabler/icons-react@npm:^3.19.0": + version: 3.19.0 + resolution: "@tabler/icons-react@npm:3.19.0" dependencies: - "@tabler/icons": "npm:3.6.0" + "@tabler/icons": "npm:3.19.0" peerDependencies: react: ">= 16" - checksum: 10c0/55a5acdddc2cbda07d8531fbc33359fc778c0887172fa0ad5944085dea95db5018988964f1105f39ff95ea34a7a434e1d29c723459d0344c28c7a57ccc655eec + checksum: 10c0/8ff8b92aae28ebeeea4d9b017103fd66cfd100f17c753afdcb4b426b6ed899a16c10ff2fb51e006d587d44bdf8ef39935a7bfa1c9898cc3dfa2c313b73b10c39 languageName: node linkType: hard -"@tabler/icons@npm:3.6.0": - version: 3.6.0 - resolution: "@tabler/icons@npm:3.6.0" - checksum: 10c0/08e4442cf1a08f31e5bf03863b2e94d1a57c5e150a19acba6f2cd2dd56a031bd35757bfc69effe7592ca1f62d530b12e25f65596a9ddc9311eff3e1e89e36f85 +"@tabler/icons@npm:3.19.0": + version: 3.19.0 + resolution: "@tabler/icons@npm:3.19.0" + checksum: 10c0/b55371b62d86e5d74d1e3986051ff94fc896ca378331ceea48bd9acb286648f7e7ea4f7261b0e3b604e33282c9815453af17185f33e169c30e89500c6006b0c0 languageName: node linkType: hard @@ -1223,6 +1245,13 @@ __metadata: languageName: node linkType: hard +"dayjs@npm:^1.11.13": + version: 1.11.13 + resolution: "dayjs@npm:1.11.13" + checksum: 10c0/a3caf6ac8363c7dade9d1ee797848ddcf25c1ace68d9fe8678ecf8ba0675825430de5d793672ec87b24a69bf04a1544b176547b2539982275d5542a7955f35b7 + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.4": version: 4.3.5 resolution: "debug@npm:4.3.5" @@ -2211,7 +2240,7 @@ __metadata: languageName: node linkType: hard -"react-router@npm:6.23.1, react-router@npm:^6.23.1": +"react-router@npm:6.23.1": version: 6.23.1 resolution: "react-router@npm:6.23.1" dependencies: @@ -2222,6 +2251,17 @@ __metadata: languageName: node linkType: hard +"react-router@npm:^6.26.2": + version: 6.26.2 + resolution: "react-router@npm:6.26.2" + dependencies: + "@remix-run/router": "npm:1.19.2" + peerDependencies: + react: ">=16.8" + checksum: 10c0/0d15a39b419c99fb5ccad76388bfc4ee2b01323b3b1b694595a9f9ea28e1fbeea25486b5398f5d3d93922f5c6a9aa751b6bb27419488d85279f6ca5ff9e0a6bb + languageName: node + linkType: hard + "react-style-singleton@npm:^2.2.1": version: 2.2.1 resolution: "react-style-singleton@npm:2.2.1" @@ -2630,24 +2670,26 @@ __metadata: version: 0.0.0-use.local resolution: "ui2@workspace:." dependencies: - "@mantine/core": "npm:^7.12.2" - "@mantine/form": "npm:^7.12.2" - "@mantine/hooks": "npm:^7.12.2" - "@mantine/notifications": "npm:^7.12.2" - "@reduxjs/toolkit": "npm:^2.2.5" - "@tabler/icons-react": "npm:^3.6.0" + "@mantine/core": "npm:^7.13.2" + "@mantine/dates": "npm:^7.13.2" + "@mantine/form": "npm:^7.13.2" + "@mantine/hooks": "npm:^7.13.2" + "@mantine/notifications": "npm:^7.13.2" + "@reduxjs/toolkit": "npm:^2.2.8" + "@tabler/icons-react": "npm:^3.19.0" "@types/js-cookie": "npm:^3" "@types/react": "npm:^18.3.3" "@types/react-dom": "npm:^18.3.0" "@types/react-router-dom": "npm:^5.3.3" "@vitejs/plugin-react": "npm:^4.3.1" axios: "npm:^1.7.2" + dayjs: "npm:^1.11.13" js-cookie: "npm:^3.0.5" prettier: "npm:^3.3.2" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" react-redux: "npm:^9.1.2" - react-router: "npm:^6.23.1" + react-router: "npm:^6.26.2" react-router-dom: "npm:^6.23.1" sass: "npm:^1.77.8" typescript: "npm:^5.4.5"