Refactor notifications to redux #6

Merged
yehoshuasandler merged 3 commits from refactor-notifications-to-redux into main 2023-09-04 10:02:08 -05:00
16 changed files with 413 additions and 159 deletions

View File

@ -8,6 +8,7 @@
"heroicons",
"konva",
"libretranslate",
"reduxjs",
"tailwindcss",
"Tesseract",
"Textualize",

View File

@ -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<HTMLFormElement>(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)
}

View File

@ -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' }))
}
}

View File

@ -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 <InformationCircleIcon className='w-6 h-6 text-blue-400' />
case 'info': return <InformationCircleIcon className='w-6 h-6 text-blue-400' />
case 'warning': return <ExclamationTriangleIcon className='w-6 h-6 text-orange-400' />
case 'error': return <ExclamationCircleIcon className='w-6 h-6 text-red-600' />
}
}
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 <>
<div
aria-live="assertive"
className="pointer-events-none absolute block top-0 left-0 w-full h-full"
>
<div className="absolute items-center" style={{ bottom: '12px', right: '16px' }}>
<Transition
show={!!currentNotification}
as={Fragment}
enter="transform ease-out duration-1300 transition"
enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="p-4">
<div className="flex items-center">
{renderIcon(currentNotification?.level)}
<div className="flex content-center flex-1 justify-between">
<p className="flex-1 text-sm font-medium text-gray-900 ml-2">{currentNotification?.message}</p>
{currentNotification?.actionButtonText ? <button
type="button"
className="ml-3 flex-shrink-0 rounded-md bg-white text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => handleOnClick()}
>
{currentNotification?.actionButtonText}
</button>
: <></>
}
</div>
<div className="ml-4 flex flex-shrink-0">
<button
type="button"
className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => {
dispatch(dismissCurrentNotification())
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</>
}
export default Notification

View File

@ -1,7 +0,0 @@
import { NotificationContextType } from './types';
const makeDefaultNotification = (): NotificationContextType => ({
addNotificationToQueue: (_) => {},
})
export default makeDefaultNotification

View File

@ -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<NotificationContextType>(makeDefaultNotification())
export function useNotification() {
return useContext(NotificationContext)
}
const notificationTimeInMilliseconds = 3000
const queue: NotificationProps[] = []
const renderIcon = (level: NotificationProps['level'] = 'info') => {
switch (level) {
default: return <InformationCircleIcon className='w-6 h-6 text-blue-400' />
case 'info': return <InformationCircleIcon className='w-6 h-6 text-blue-400' />
case 'warning': return <ExclamationTriangleIcon className='w-6 h-6 text-orange-400' />
case 'error': return <ExclamationCircleIcon className='w-6 h-6 text-red-600' />
}
}
type Props = { children: ReactNode }
export function NotificationProvider({ children }: Props) {
const [currentNotification, setCurrentNotification] = useState<NotificationProps | undefined>()
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 <NotificationContext.Provider value={value}>
{ children }
<>
<div
aria-live="assertive"
className="pointer-events-none absolute block top-0 left-0 w-full h-full"
>
<div className="absolute items-center" style={{ bottom: '12px', right: '16px' }}>
<Transition
show={!!currentNotification}
as={Fragment}
enter="transform ease-out duration-1300 transition"
enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="p-4">
<div className="flex items-center">
{renderIcon(currentNotification?.level)}
<div className="flex w-0 content-center flex-1 justify-between">
<p className="w-0 flex-1 text-sm font-medium text-gray-900 ml-2">{currentNotification?.message}</p>
{currentNotification?.actionButtonText ? <button
type="button"
className="ml-3 flex-shrink-0 rounded-md bg-white text-sm font-medium text-indigo-600 hover:text-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => handleOnClick()}
>
{currentNotification?.actionButtonText}
</button>
: <></>
}
</div>
<div className="ml-4 flex flex-shrink-0">
<button
type="button"
className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => {
dismissCurrentNotification()
}}
>
<span className="sr-only">Close</span>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</>
</NotificationContext.Provider>
}

View File

@ -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",

View File

@ -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",

View File

@ -1 +1 @@
e331f957a49840160190db6ea894d0b5
bf8d6eeb2add78baa4092415a836f1ad

View File

@ -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 <div className='min-h-screen' >
<NavigationProvider navigationProps={initialNavigationProps}>
<ProjectProvider projectProps={initialProjectProps}>
<NotificationProvider>
<Component {...pageProps} />
</NotificationProvider>
<Providers>
<Component {...pageProps} />
</Providers>
</ProjectProvider>
</NavigationProvider>
</div>

View File

@ -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 <>
<MainHead />
{renderSelectedMainPage()}
<Notification />
</>
}

View File

@ -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<NotificationProps[]>) => {
state.queue = action.payload
},
setCurrentNotification: (state, action: PayloadAction<NotificationProps | undefined>) => {
state.currentNotification = action.payload
},
pushNotification: (state, action: PayloadAction<NotificationProps>) => {
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

View File

@ -9,6 +9,7 @@ export type NotificationProps = {
level?: NotificationLevel,
}
export type NotificationContextType = {
addNotificationToQueue: (notificationProps: NotificationProps) => void
export type NotificationQueueState = {
queue: NotificationProps[],
currentNotification?: NotificationProps
}

5
frontend/redux/hooks.ts Normal file
View File

@ -0,0 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

View File

@ -0,0 +1,8 @@
'use client'
import { store } from './store'
import { Provider } from 'react-redux'
export function Providers({ children }: { children: React.ReactNode }) {
return <Provider store={store}>{children}</Provider>
}

16
frontend/redux/store.ts Normal file
View File

@ -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<typeof store.getState>
export type AppDispatch = typeof store.dispatch
setupListeners(store.dispatch)