diff --git a/.vscode/settings.json b/.vscode/settings.json index b0ed334..9a8706e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "heroicons", "konva", "libretranslate", + "reduxjs", "tailwindcss", "Tesseract", "Textualize", diff --git a/frontend/components/DocumentCanvas/AreaContextMenu/index.tsx b/frontend/components/DocumentCanvas/AreaContextMenu/index.tsx index 1c5eee6..20b4bf3 100644 --- a/frontend/components/DocumentCanvas/AreaContextMenu/index.tsx +++ b/frontend/components/DocumentCanvas/AreaContextMenu/index.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useRef, useState } from 'react' +import React, { useState } from 'react' import { entities } from '../../../wailsjs/wailsjs/go/models' import { Html } from 'react-konva-utils' import { ClipboardIcon, ArrowPathIcon, TrashIcon, LanguageIcon } from '@heroicons/react/24/outline' @@ -9,9 +9,10 @@ import { useProject } from '../../../context/Project/provider' import asyncClick from '../../../utils/asyncClick' import processImageArea from '../../../useCases/processImageArea' import classNames from '../../../utils/classNames' -import { useNotification } from '../../../context/Notification/provider' import LanguageSelect from '../../utils/LanguageSelect' import { RequestTranslateArea } from '../../../wailsjs/wailsjs/go/ipc/Channel' +import { useDispatch } from 'react-redux' +import { pushNotification } from '../../../redux/features/notifications/notificationQueueSlice' type Props = { x: number, @@ -23,9 +24,8 @@ type Props = { const AreaContextMenu = (props: Props) => { + const dispatch = useDispatch() const { getProcessedAreaById, requestDeleteAreaById, getSelectedDocument, requestUpdateArea } = useProject() - const { addNotificationToQueue } = useNotification() - const formRef = useRef(null) const [shouldShowProcessLanguageSelect, setShouldShowProcessLanguageSelect] = useState(false) const { area, setIsAreaContextMenuOpen, scale, x, y } = props @@ -36,15 +36,15 @@ const AreaContextMenu = (props: Props) => { const wordsOfProcessedArea = processedArea?.lines.flatMap(l => l.words.map(w => w.fullText)) const fullText = wordsOfProcessedArea?.join(' ') if (!fullText) { - addNotificationToQueue({ message: 'No text found to copy.', level: 'warning' }) + dispatch(pushNotification({ message: 'No text found to copy.', level: 'warning' })) return } try { await navigator.clipboard.writeText(fullText) - addNotificationToQueue({ message: 'Copied area to clipboard' }) + dispatch(pushNotification({ message: 'Copied area to clipboard' })) } catch (err) { - addNotificationToQueue({ message: 'Error copying area', level: 'error' }) + dispatch(pushNotification({ message: 'Error copying area', level: 'error' })) } } @@ -53,9 +53,9 @@ const AreaContextMenu = (props: Props) => { try { const response = await requestDeleteAreaById(area.id) - if (!response) addNotificationToQueue({ message: 'Could not delete area', level: 'warning' }) + if (!response) dispatch(pushNotification({ message: 'Could not delete area', level: 'warning' })) } catch (err) { - addNotificationToQueue({ message: 'Error deleting area', level: 'error' }) + dispatch(pushNotification({ message: 'Error deleting area', level: 'error' })) } } @@ -64,17 +64,17 @@ const AreaContextMenu = (props: Props) => { const documentId = getSelectedDocument()?.id if (!documentId) { - addNotificationToQueue({ message: 'Issue finding selected document', level: 'warning' }) + dispatch(pushNotification({ message: 'Issue finding selected document', level: 'warning' })) return } try { - addNotificationToQueue({ message: 'Processing test of area' }) + dispatch(pushNotification({ message: 'Processing test of area' })) const response = await processImageArea(documentId, area.id) - if (response) addNotificationToQueue({ message: 'Area successfully processed' }) - else addNotificationToQueue({ message: 'No text result from processing area', level: 'warning' }) + if (response) dispatch(pushNotification({ message: 'Area successfully processed' })) + else dispatch(pushNotification({ message: 'No text result from processing area', level: 'warning' })) } catch (err) { - addNotificationToQueue({ message: 'Error processing area', level: 'error' }) + dispatch(pushNotification({ message: 'Error processing area', level: 'error' })) } } @@ -84,10 +84,10 @@ const AreaContextMenu = (props: Props) => { try { const wasSuccessful = await RequestTranslateArea(area.id) - if (wasSuccessful) addNotificationToQueue({ message: 'Successfully translated area' }) - else addNotificationToQueue({ message: 'Issue translating area', level: 'warning' }) + if (wasSuccessful) dispatch(pushNotification({ message: 'Successfully translated area' })) + else dispatch(pushNotification({ message: 'Issue translating area', level: 'warning' })) } catch (err) { - addNotificationToQueue({ message: 'Error translating area', level: 'error' }) + dispatch(pushNotification({ message: 'Error translating area', level: 'error' })) } } @@ -98,26 +98,25 @@ const AreaContextMenu = (props: Props) => { try { successfullyUpdatedLanguageOnArea = await requestUpdateArea({...area, ...{language: selectedLanguage}}) } catch (err) { - addNotificationToQueue({ message: 'Error updating area language', level: 'error' }) + dispatch(pushNotification({ message: 'Error updating area language', level: 'error' })) return } const selectedDocumentId = getSelectedDocument()?.id if (!successfullyUpdatedLanguageOnArea || !selectedDocumentId) { - addNotificationToQueue({ message: 'Did not successfully update area language', level: 'warning' }) + dispatch(pushNotification({ message: 'Did not successfully update area language', level: 'warning' })) return } try { await processImageArea(selectedDocumentId, area.id) - addNotificationToQueue({ message: 'Finished processing area', level: 'info' }) + dispatch(pushNotification({ message: 'Finished processing area', level: 'info' })) } catch (err) { - addNotificationToQueue({ message: 'Error processing area', level: 'error' }) + dispatch(pushNotification({ message: 'Error processing area', level: 'error' })) } } const handleOnBlur = (e: React.FocusEvent) => { - console.log(e.relatedTarget) e.preventDefault() if (e.relatedTarget === null) setIsAreaContextMenuOpen(false) } diff --git a/frontend/components/DocumentCanvas/ToolingOverlay/index.tsx b/frontend/components/DocumentCanvas/ToolingOverlay/index.tsx index bea910b..bf29a6b 100644 --- a/frontend/components/DocumentCanvas/ToolingOverlay/index.tsx +++ b/frontend/components/DocumentCanvas/ToolingOverlay/index.tsx @@ -1,19 +1,20 @@ 'use client' import React, { useEffect, useState } from 'react' +import { useDispatch } from 'react-redux' import { DocumentTextIcon, LanguageIcon, LinkIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, SquaresPlusIcon } from '@heroicons/react/24/outline' import { useProject } from '../../../context/Project/provider' import { entities } from '../../../wailsjs/wailsjs/go/models' import LanguageSelect from '../../utils/LanguageSelect' import { useStage } from '../context/provider' import ToolToggleButton from './ToolToggleButton' -import { useNotification } from '../../../context/Notification/provider' import processImageArea from '../../../useCases/processImageArea' +import { pushNotification } from '../../../redux/features/notifications/notificationQueueSlice' const ToolingOverlay = () => { + const dispatch = useDispatch() const { getSelectedDocument, selectedAreaId, requestUpdateArea, requestUpdateDocument, updateDocuments } = useProject() - const { addNotificationToQueue } = useNotification() const { scale, scaleStep, maxScale, setScale, isLinkAreaContextsVisible, setIsLinkAreaContextsVisible, @@ -36,22 +37,22 @@ const ToolingOverlay = () => { try { successfullyUpdatedLanguageOnArea = await requestUpdateArea({ ...selectedArea, ...{ language: selectedLanguage } }) } catch (err) { - addNotificationToQueue({ message: 'Error updating area language', level: 'error' }) + dispatch(pushNotification({ message: 'Error updating area language', level: 'error' })) return } const selectedDocumentId = getSelectedDocument()?.id if (!successfullyUpdatedLanguageOnArea || !selectedDocumentId) { - addNotificationToQueue({ message: 'Did not successfully update area language', level: 'warning' }) + dispatch(pushNotification({ message: 'Did not successfully update area language', level: 'warning' })) return } try { await processImageArea(selectedDocumentId, selectedArea.id) await updateDocuments() - addNotificationToQueue({ message: 'Finished processing area', level: 'info' }) + dispatch(pushNotification({ message: 'Finished processing area', level: 'info' })) } catch (err) { - addNotificationToQueue({ message: 'Error processing area', level: 'error' }) + dispatch(pushNotification({ message: 'Error processing area', level: 'error' })) } } diff --git a/frontend/components/Notifications/index.tsx b/frontend/components/Notifications/index.tsx new file mode 100644 index 0000000..34e7af7 --- /dev/null +++ b/frontend/components/Notifications/index.tsx @@ -0,0 +1,86 @@ +import { Fragment, useEffect } from 'react' +import { Transition } from '@headlessui/react' +import { useDispatch, useSelector } from 'react-redux' +import { XMarkIcon, InformationCircleIcon, ExclamationTriangleIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline' +import { RootState } from '../../redux/store' +import { NotificationProps } from '../../redux/features/notifications/types' +import { dismissCurrentNotification } from '../../redux/features/notifications/notificationQueueSlice' + +const renderIcon = (level: NotificationProps['level'] = 'info') => { + switch (level) { + default: return + case 'info': return + case 'warning': return + case 'error': return + } +} + +const notificationTime = 3000 + +const Notification = () => { + const { currentNotification, queue } = useSelector((state: RootState) => state.notificationQueue) + const dispatch = useDispatch() + + const handleOnClick = () => { + if (currentNotification?.onActionClickCallback) currentNotification?.onActionClickCallback() + if (currentNotification?.closeOnAction) dispatch(dismissCurrentNotification()) + } + + useEffect(() => { + if (queue.length) setTimeout(() => dispatch(dismissCurrentNotification()), notificationTime) + }, [currentNotification]) + + return <> +
+
+ +
+
+
+ {renderIcon(currentNotification?.level)} +
+

{currentNotification?.message}

+ {currentNotification?.actionButtonText ? + : <> + } +
+
+ +
+
+
+
+
+
+
+ +} + +export default Notification diff --git a/frontend/context/Notification/makeDefaultNotification.ts b/frontend/context/Notification/makeDefaultNotification.ts deleted file mode 100644 index a668a31..0000000 --- a/frontend/context/Notification/makeDefaultNotification.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NotificationContextType } from './types'; - -const makeDefaultNotification = (): NotificationContextType => ({ - addNotificationToQueue: (_) => {}, -}) - -export default makeDefaultNotification diff --git a/frontend/context/Notification/provider.tsx b/frontend/context/Notification/provider.tsx deleted file mode 100644 index 491c824..0000000 --- a/frontend/context/Notification/provider.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client' - -import { createContext, Fragment, ReactNode, useContext, useEffect, useState } from 'react' -import { XMarkIcon, InformationCircleIcon, ExclamationTriangleIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline' -import { NotificationContextType, NotificationProps } from './types' -import makeDefaultNotification from './makeDefaultNotification' -import { Transition } from '@headlessui/react' - -const NotificationContext = createContext(makeDefaultNotification()) - -export function useNotification() { - return useContext(NotificationContext) -} - -const notificationTimeInMilliseconds = 3000 -const queue: NotificationProps[] = [] - -const renderIcon = (level: NotificationProps['level'] = 'info') => { - switch (level) { - default: return - case 'info': return - case 'warning': return - case 'error': return - } -} - -type Props = { children: ReactNode } -export function NotificationProvider({ children }: Props) { - const [currentNotification, setCurrentNotification] = useState() - - const addNotificationToQueue = (notificationProps: NotificationProps) => { - if (!queue.length) { - queue.push(notificationProps) - setCurrentNotification(notificationProps) - } else { - queue.push(notificationProps) - } - } - - const dismissCurrentNotification = () => { - queue.shift() - setCurrentNotification(queue[0]) - } - - useEffect(() => { - if (!queue.length) return - setTimeout(dismissCurrentNotification, notificationTimeInMilliseconds) - }, [currentNotification]) - - const handleOnClick = () => { - if (currentNotification?.onActionClickCallback) currentNotification?.onActionClickCallback() - if (currentNotification?.closeOnAction) dismissCurrentNotification() - } - - - const value = { addNotificationToQueue } - - return - { children } - <> -
-
- -
-
-
- {renderIcon(currentNotification?.level)} -
-

{currentNotification?.message}

- {currentNotification?.actionButtonText ? - : <> - } -
-
- -
-
-
-
-
-
-
- - -
-} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5f3487e..09a810e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", "@monaco-editor/react": "^4.4.6", + "@reduxjs/toolkit": "^1.9.5", "@tailwindcss/forms": "^0.5.3", "konva": "^9.2.0", "next": "^13.4.4", @@ -19,6 +20,7 @@ "react-konva": "^18.2.9", "react-konva-utils": "^1.0.4", "react-markdown": "^8.0.5", + "react-redux": "^8.1.2", "rehype-raw": "^6.1.1", "tesseract.js": "^4.0.2", "use-image": "^1.1.0", @@ -214,7 +216,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -633,6 +634,29 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -674,6 +698,15 @@ "@types/unist": "*" } }, + "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==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -723,7 +756,7 @@ "version": "18.0.10", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -746,6 +779,11 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -2588,6 +2626,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/html-void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", @@ -2622,6 +2668,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4433,6 +4488,49 @@ "react": "^18.2.0" } }, + "node_modules/react-redux": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", + "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4452,6 +4550,22 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -4528,6 +4642,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -5288,6 +5407,14 @@ "react-dom": ">=16.8.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5630,7 +5757,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.11" } @@ -5895,6 +6021,17 @@ "tslib": "^2.4.0" } }, + "@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, "@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -5933,6 +6070,15 @@ "@types/unist": "*" } }, + "@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==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -5982,7 +6128,7 @@ "version": "18.0.10", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", - "dev": true, + "devOptional": true, "requires": { "@types/react": "*" } @@ -6005,6 +6151,11 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -7317,6 +7468,14 @@ "space-separated-tokens": "^2.0.0" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "html-void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", @@ -7341,6 +7500,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8446,6 +8610,26 @@ "scheduler": "^0.23.0" } }, + "react-redux": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", + "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "requires": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -8462,6 +8646,20 @@ "picomatch": "^2.2.1" } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "requires": {} + }, "regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -8514,6 +8712,11 @@ "unified": "^10.0.0" } }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -9045,6 +9248,12 @@ "integrity": "sha512-+cBHRR/44ZyMUS873O0vbVylgMM0AbdTunEplAWXvIQ2p69h2sIo2Qq74zeUsq6AMo+27e5lERQvXzd1crGiMg==", "requires": {} }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 27af04d..4f017c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", "@monaco-editor/react": "^4.4.6", + "@reduxjs/toolkit": "^1.9.5", "@tailwindcss/forms": "^0.5.3", "konva": "^9.2.0", "next": "^13.4.4", @@ -24,6 +25,7 @@ "react-konva": "^18.2.9", "react-konva-utils": "^1.0.4", "react-markdown": "^8.0.5", + "react-redux": "^8.1.2", "rehype-raw": "^6.1.1", "tesseract.js": "^4.0.2", "use-image": "^1.1.0", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 549dcc1..56273da 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -e331f957a49840160190db6ea894d0b5 \ No newline at end of file +bf8d6eeb2add78baa4092415a836f1ad \ No newline at end of file diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 46b4f0e..9bdb5ac 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -7,7 +7,7 @@ import { entities } from '../wailsjs/wailsjs/go/models' import '../styles/globals.css' import { NavigationProvider } from '../context/Navigation/provider' import { mainPages, workspaces } from '../context/Navigation/types' -import { NotificationProvider } from '../context/Notification/provider' +import { Providers } from '../redux/provider' const initialProjectProps = { id: '', @@ -24,9 +24,9 @@ export default function MainAppLayout({ Component, pageProps }: AppProps) { return
- - - + + +
diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 0ebc6de..7f7a1dd 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -7,6 +7,7 @@ import Navigation from '../components/workspace/Navigation' import { useNavigation } from '../context/Navigation/provider' import { mainPages } from '../context/Navigation/types' import { useProject } from '../context/Project/provider' +import Notification from '../components/Notifications' const Home: NextPage = () => { const { currentSession } = useProject() @@ -27,6 +28,7 @@ const Home: NextPage = () => { return <> {renderSelectedMainPage()} + } diff --git a/frontend/redux/features/notifications/notificationQueueSlice.ts b/frontend/redux/features/notifications/notificationQueueSlice.ts new file mode 100644 index 0000000..3c59ac0 --- /dev/null +++ b/frontend/redux/features/notifications/notificationQueueSlice.ts @@ -0,0 +1,44 @@ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import { NotificationProps, NotificationQueueState } from './types' + +const initialState: NotificationQueueState = { + currentNotification: undefined, + queue: [] +} + +export const notificationQueueSlice = createSlice({ + name: 'propertyList', + initialState, + reducers: { + setNotifications: (state, action: PayloadAction) => { + state.queue = action.payload + }, + setCurrentNotification: (state, action: PayloadAction) => { + state.currentNotification = action.payload + }, + pushNotification: (state, action: PayloadAction) => { + let { queue } = state + const { payload: newNotification } = action + + if (queue.length) queue.push(newNotification) + else { + queue.push(newNotification) + state.currentNotification = newNotification + } + }, + dismissCurrentNotification: (state) => { + state.queue.shift() + state.currentNotification = state.queue[0] || undefined + } + } +}) + +export const { + setNotifications, + setCurrentNotification, + pushNotification, + dismissCurrentNotification +} = notificationQueueSlice.actions + +export default notificationQueueSlice.reducer \ No newline at end of file diff --git a/frontend/context/Notification/types.ts b/frontend/redux/features/notifications/types.ts similarity index 70% rename from frontend/context/Notification/types.ts rename to frontend/redux/features/notifications/types.ts index 8d03953..dbb94f4 100644 --- a/frontend/context/Notification/types.ts +++ b/frontend/redux/features/notifications/types.ts @@ -9,6 +9,7 @@ export type NotificationProps = { level?: NotificationLevel, } -export type NotificationContextType = { - addNotificationToQueue: (notificationProps: NotificationProps) => void +export type NotificationQueueState = { + queue: NotificationProps[], + currentNotification?: NotificationProps } diff --git a/frontend/redux/hooks.ts b/frontend/redux/hooks.ts new file mode 100644 index 0000000..8207d5e --- /dev/null +++ b/frontend/redux/hooks.ts @@ -0,0 +1,5 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import type { RootState, AppDispatch } from './store' + +export const useAppDispatch = () => useDispatch() +export const useAppSelector: TypedUseSelectorHook = useSelector \ No newline at end of file diff --git a/frontend/redux/provider.tsx b/frontend/redux/provider.tsx new file mode 100644 index 0000000..fb61cf1 --- /dev/null +++ b/frontend/redux/provider.tsx @@ -0,0 +1,8 @@ +'use client' + +import { store } from './store' +import { Provider } from 'react-redux' + +export function Providers({ children }: { children: React.ReactNode }) { + return {children} +} \ No newline at end of file diff --git a/frontend/redux/store.ts b/frontend/redux/store.ts new file mode 100644 index 0000000..33e9063 --- /dev/null +++ b/frontend/redux/store.ts @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit' +import notificationQueueSlice from './features/notifications/notificationQueueSlice' +import { setupListeners } from '@reduxjs/toolkit/dist/query' + +export const store = configureStore({ + reducer: { + notificationQueue: notificationQueueSlice, + }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), +}) + +export type RootState = ReturnType + +export type AppDispatch = typeof store.dispatch + +setupListeners(store.dispatch)