diff --git a/client/package.json b/client/package.json index 4b38b0c66..955b31121 100644 --- a/client/package.json +++ b/client/package.json @@ -18,10 +18,12 @@ "@material-ui/icons": "^4.11.2", "@monaco-editor/react": "^4.2.1", "@rebass/forms": "^4.0.6", + "@reduxjs/toolkit": "^1.6.2", "@types/jest": "^26.0.24", "@types/node": "^16.4.3", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", + "@types/react-redux": "^7.1.18", "classnames": "^2.3.1", "deep-equal": "^2.0.5", "eslint-config-react-app": "^6.0.0", @@ -42,9 +44,11 @@ "react-hotkeys-hook": "^3.3.2", "react-icons": "^4.2.0", "react-pouchdb": "^2.1.0", + "react-redux": "^7.2.5", "react-select": "^4.3.1", "react-simple-code-editor": "^0.11.0", "rebass": "^4.0.7", + "redux": "^4.1.1", "regenerator-runtime": "^0.13.9", "rete-connection-reroute-plugin": "^0.4.0", "rete-context-menu-plugin": "^0.6.0-rc.1", diff --git a/client/src/contexts/EditorProvider.tsx b/client/src/contexts/EditorProvider.tsx index 0c5c2f45c..d842862bb 100644 --- a/client/src/contexts/EditorProvider.tsx +++ b/client/src/contexts/EditorProvider.tsx @@ -1,42 +1,21 @@ -import { initEditor, EngineContext } from '@latitudegames/thoth-core' -import React, { useRef, useContext, createContext, useState } from 'react' +import { initEditor } from '@latitudegames/thoth-core' +import React, { + useRef, + useContext, + createContext, + useState, + useEffect, +} from 'react' + +import { useLazyGetSpellQuery, Spell } from '../state/spells' import LoadingScreen from '../features/common/LoadingScreen/LoadingScreen' import { MyNode } from '../features/common/Node/Node' import gridimg from '../grid.png' -import { usePubSub } from './PubSubProvider' -import { useRete } from './ReteProvider' import { useSpell } from './SpellProvider' - -export type SpellContext = { - currentSpell: {} - getCurrentSpell: () => void - updateCurrentSpell: {} - loadSpell: () => void - saveSpell: () => void - newSpell: () => void - saveCurrentSpell: () => void - stateHistory: never[] - currentGameState: {} - getCurrentGameState: () => Record - rewriteCurrentGameState: () => Record - updateCurrentGameState: () => void - getThothVersion: () => void -} - -export interface ReteContext extends EngineContext { - onInspector: () => void - onPlayTest: () => void - onGameState: () => void - sendToPlaytest: () => void - sendToInspector: () => void - clearTextEditor: () => void - getSpell: () => void - getModule: () => void - getGameState: () => void - setGameState: () => void - getModules: () => void -} +import { usePubSub } from './PubSubProvider' +import { useRete, ReteContext } from './ReteProvider' +// import { ThothTab } from './TabManagerProvider' export type ThothTab = { layoutJson: string @@ -54,7 +33,8 @@ const Context = createContext({ serialize: () => {}, buildEditor: ( el: HTMLDivElement, - spell: SpellContext, + // todo update this to use proper spell type + spell: Spell | undefined, tab: ThothTab, reteInterface: ReteContext ) => {}, @@ -89,10 +69,10 @@ const EditorProvider = ({ children }) => { return editorRef.current } - const buildEditor = async (container, spell, tab, thoth) => { - // console.log('init editor', initEditor) + const buildEditor = async (container, _spell, tab, thoth) => { + // copy spell in case it is read only + const spell = JSON.parse(JSON.stringify(_spell)) // eslint-disable-next-line no-console - console.log('building editor for tab', tab) const newEditor = await initEditor({ container, pubSub, @@ -106,10 +86,7 @@ const EditorProvider = ({ children }) => { // set editor to the map setEditor(newEditor) - if (tab.type === 'spell') { - const spellDoc = await thoth.getSpell(tab.spell) - newEditor.loadGraph(spellDoc.toJSON().graph) - } + if (tab.type === 'spell') newEditor.loadGraph(spell.graph) if (tab.type === 'module') { const moduleDoc = await thoth.getModule(tab.module) @@ -169,14 +146,20 @@ const EditorProvider = ({ children }) => { } const RawEditor = ({ tab, children }) => { + const [getSpell, { data: spell, isLoading }] = useLazyGetSpellQuery() const [loaded, setLoaded] = useState(false) const { buildEditor } = useEditor() - const spell = useSpell() - const { getCurrentGameState, updateCurrentGameState } = spell + const { getCurrentGameState, updateCurrentGameState } = useSpell() // This will be the main interface between thoth and rete const reteInterface = useRete() - if (!tab) return + useEffect(() => { + if (!tab) return + + getSpell(tab.spell) + }, [tab]) + + if (isLoading || !tab || !spell) return return ( <> @@ -214,6 +197,6 @@ const RawEditor = ({ tab, children }) => { export const Editor = React.memo(RawEditor) -Editor.whyDidYouRender = true +Editor.whyDidYouRender = false export default EditorProvider diff --git a/client/src/contexts/ReteProvider.tsx b/client/src/contexts/ReteProvider.tsx index 82399e884..f1b87afb7 100644 --- a/client/src/contexts/ReteProvider.tsx +++ b/client/src/contexts/ReteProvider.tsx @@ -1,3 +1,4 @@ +import { EngineContext } from '@latitudegames/thoth-core' import { useContext, createContext } from 'react' import { postEnkiCompletion } from '../services/game-api/enki' @@ -11,6 +12,20 @@ Some notes here. The new rete provider, not to be confused with the old rete pr Not all functions will be needed on the server, and functions which are not will be labeled as such. */ +export interface ReteContext extends EngineContext { + onInspector: () => void + onPlayTest: () => void + onGameState: () => void + sendToPlaytest: () => void + sendToInspector: () => void + clearTextEditor: () => void + getSpell: () => void + getModule: () => void + getGameState: () => void + setGameState: () => void + getModules: () => void +} + const Context = createContext({ onInspector: () => {}, onPlayTest: () => {}, @@ -134,6 +149,8 @@ const ReteProvider = ({ children, tab }) => { enkiCompletion, huggingface, ...modules, + + // going to need to manuall create theses ...spells, } diff --git a/client/src/contexts/SpellProvider.tsx b/client/src/contexts/SpellProvider.tsx index ca7575bd1..00e782f85 100644 --- a/client/src/contexts/SpellProvider.tsx +++ b/client/src/contexts/SpellProvider.tsx @@ -3,13 +3,25 @@ import { useContext, createContext, useState, useRef } from 'react' import { useDB } from './DatabaseProvider' +export type SpellContext = { + currentSpell: {} + getCurrentSpell: () => void + loadSpell: () => void + saveSpell: () => void + saveCurrentSpell: () => void + stateHistory: never[] + currentGameState: {} + getCurrentGameState: () => Record + rewriteCurrentGameState: () => Record + updateCurrentGameState: () => void + getThothVersion: () => void +} + const Context = createContext({ currentSpell: {}, getCurrentSpell: () => {}, - updateCurrentSpell: {}, loadSpell: () => {}, saveSpell: () => {}, - newSpell: () => {}, saveCurrentSpell: () => {}, stateHistory: [], currentGameState: {}, diff --git a/client/src/database/index.js b/client/src/database/index.js index f7b2d3b0f..01e255f1e 100644 --- a/client/src/database/index.js +++ b/client/src/database/index.js @@ -14,12 +14,13 @@ import userCollection from './schemas/user' // eslint-disable-next-line @typescript-eslint/no-var-requires addRxPlugin(require('pouchdb-adapter-idb')) +let databaseReturn = null let database = null const databaseName = 'thoth_alpha' const adapter = 'idb' export const initDB = async () => { - if (database !== null) return database + if (databaseReturn !== null) return databaseReturn // Uncomment this for fast deletion of DB if (process.env.NODE_ENV !== 'production') { @@ -107,8 +108,10 @@ export const initDB = async () => { user: userModel(database), } - return { + databaseReturn = { database, models, } + + return databaseReturn } diff --git a/client/src/database/models/spellModel.ts b/client/src/database/models/spellModel.ts index cea3da8cf..cf5c14e75 100644 --- a/client/src/database/models/spellModel.ts +++ b/client/src/database/models/spellModel.ts @@ -9,6 +9,10 @@ const loadSpellModel = db => { .exec() } + const getSpells = async spellId => { + return await db.spells.find().exec() + } + const updateSpell = async (spellId, update) => { const spell = await getSpell(spellId) @@ -20,6 +24,17 @@ const loadSpellModel = db => { }) } + const saveSpell = async (spellId, spellUpdate) => { + const spell = await getSpell(spellId) + + return spell.atomicUpdate(oldData => { + return { + ...oldData, + ...spellUpdate, + } + }) + } + const newSpell = async ({ graph, name, gameState = {} }) => { const newSpell = { name, @@ -32,7 +47,9 @@ const loadSpellModel = db => { return { getSpell, + getSpells, updateSpell, + saveSpell, newSpell, } } diff --git a/client/src/features/StartScreen/StartScreen.js b/client/src/features/StartScreen/StartScreen.js index fc2dd1260..0b94832a8 100644 --- a/client/src/features/StartScreen/StartScreen.js +++ b/client/src/features/StartScreen/StartScreen.js @@ -1,5 +1,6 @@ import React, { useState } from 'react' +import { useNewSpellMutation } from '../../state/spells' import { useDB } from '../../contexts/DatabaseProvider' import { useTabManager } from '../../contexts/TabManagerProvider' import AllProjects from './components/AllProjects' @@ -15,6 +16,8 @@ const StartScreen = ({ createNew, allProjects }) => { } = useDB() const { openTab } = useTabManager() + const [newSpell] = useNewSpellMutation() + const projects = [ { label: 'Lorem ipsum' }, { label: 'Dolor sit' }, @@ -27,9 +30,7 @@ const StartScreen = ({ createNew, allProjects }) => { // TODO check for proper values here and throw errors const existingSpell = await spells.getSpell(spellData.name) - const spell = existingSpell - ? existingSpell - : await spells.newSpell(spellData) + const spell = existingSpell ? existingSpell : await newSpell(spellData) // Load modules from the spell if (spellData?.modules && spellData.modules.length > 0) diff --git a/client/src/features/StartScreen/components/CreateNew.jsx b/client/src/features/StartScreen/components/CreateNew.jsx index 85007413d..87c7a60aa 100644 --- a/client/src/features/StartScreen/components/CreateNew.jsx +++ b/client/src/features/StartScreen/components/CreateNew.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { uniqueNamesGenerator, adjectives, @@ -6,7 +6,7 @@ import { } from 'unique-names-generator' import { useLocation } from 'wouter' -import { useSpell } from '../../../contexts/SpellProvider' +import { useNewSpellMutation } from '../../../state/spells' import { useTabManager } from '../../../contexts/TabManagerProvider' import Panel from '../../common/Panel/Panel' import emptyImg from '../empty.png' @@ -35,12 +35,14 @@ const templates = [ const CreateNew = () => { const [, setLocation] = useLocation() const [selectedTemplate, setSelectedTemplate] = useState(null) - const { newSpell } = useSpell() + + const [newSpell] = useNewSpellMutation() + const { openTab, clearTabs } = useTabManager() const onCreate = async () => { const placeholderName = uniqueNamesGenerator(customConfig) - const spell = await newSpell({ + const { data: spell } = await newSpell({ graph: defaultGraph, name: placeholderName, }) diff --git a/client/src/features/StartScreen/components/OpenProject.jsx b/client/src/features/StartScreen/components/OpenProject.jsx index 096fb72c9..a38ad7fdb 100644 --- a/client/src/features/StartScreen/components/OpenProject.jsx +++ b/client/src/features/StartScreen/components/OpenProject.jsx @@ -16,6 +16,7 @@ const OpenProject = ({ loadFile, }) => { const { tabs } = useTabManager() + // TODO remove thoth version from spellprovider const { getThothVersion } = useSpell() // eslint-disable-next-line no-unused-vars const [, setLocation] = useLocation() diff --git a/client/src/features/Thoth/components/DataControls/ModuleSelect.tsx b/client/src/features/Thoth/components/DataControls/ModuleSelect.tsx index 4bd4a305f..814a24703 100644 --- a/client/src/features/Thoth/components/DataControls/ModuleSelect.tsx +++ b/client/src/features/Thoth/components/DataControls/ModuleSelect.tsx @@ -1,10 +1,15 @@ import { useSnackbar } from 'notistack' +import { useAppDispatch } from '../../../../state/hooks' +import { tabOpened } from '../../../../state/tabs' import { useModule } from '../../../../contexts/ModuleProvider' import { useTabManager } from '../../../../contexts/TabManagerProvider' import Select from '../../../common/Select/Select' const ModuleSelect = ({ control, updateData, initialValue }) => { + // const activeTab = useAppSelector(activeTabSelector) + const dispatch = useAppDispatch() + const { modules, newModule, findOneModule } = useModule() const { openTab } = useTabManager() const { enqueueSnackbar } = useSnackbar() @@ -17,6 +22,18 @@ const ModuleSelect = ({ control, updateData, initialValue }) => { })) } + const _openTab = async module => { + const tab = { + name: module.name, + type: 'module', + moduleName: module.name, + openNew: false, + } + + dispatch(tabOpened(tab)) + await openTab(tab) + } + const onChange = async ({ value }) => { const _module = await findOneModule({ name: value }) if (!_module) { @@ -29,12 +46,14 @@ const ModuleSelect = ({ control, updateData, initialValue }) => { const module = _module.toJSON() - await openTab({ - name: value, - type: 'module', - moduleName: module.name, - openNew: false, - }) + await _openTab(module) + + // await openTab({ + // name: value, + // type: 'module', + // moduleName: module.name, + // openNew: false, + // }) } const update = update => { @@ -44,12 +63,15 @@ const ModuleSelect = ({ control, updateData, initialValue }) => { const onCreateOption = async value => { try { const module = await newModule({ name: value }) - await openTab({ - name: value, - type: 'module', - moduleName: module.name, - openNew: false, - }) + + await _openTab(module) + + // await openTab({ + // name: value, + // type: 'module', + // moduleName: module.name, + // openNew: false, + // }) // todo better naming for rete modules. // Handle displaying name as using ID for internal mapping diff --git a/client/src/features/Thoth/components/EventHandler.jsx b/client/src/features/Thoth/components/EventHandler.jsx index b33b43a1f..6734332b4 100644 --- a/client/src/features/Thoth/components/EventHandler.jsx +++ b/client/src/features/Thoth/components/EventHandler.jsx @@ -1,16 +1,31 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' +import { + useSaveSpellMutation, + useGetSpellsQuery, + selectSpellById, +} from '../../../state/spells' import { useEditor } from '../../../contexts/EditorProvider' import { useLayout } from '../../../contexts/LayoutProvider' import { useModule } from '../../../contexts/ModuleProvider' -import { useSpell } from '../../../contexts/SpellProvider' +import { useSelector } from 'react-redux' const EventHandler = ({ pubSub, tab }) => { // only using this to handle events, so not rendering anything with it. const { createOrFocus, windowTypes } = useLayout() + const [saveSpellMutation] = useSaveSpellMutation() + const { data: spellsData } = useGetSpellsQuery() + const spell = useSelector(state => selectSpellById(state, tab?.spell)) + + // Spell ref because callbacks cant hold values from state without them + const spellRef = useRef(null) + useEffect(() => { + if (!spell) return + spellRef.current = spell + }, [spell]) + const { serialize, getEditor, undo, redo } = useEditor() - const { saveCurrentSpell, getSpell, getCurrentSpell } = useSpell() const { getSpellModules } = useModule() const { events, subscribe } = pubSub @@ -29,8 +44,10 @@ const EventHandler = ({ pubSub, tab }) => { } = events const saveSpell = async () => { - const graph = serialize() - await saveCurrentSpell({ graph }) + const currentSpell = spellRef.current + const graph = serialize(currentSpell) + + await saveSpellMutation({ ...currentSpell, graph }) } const createStateManager = () => { @@ -63,10 +80,9 @@ const EventHandler = ({ pubSub, tab }) => { } const onExport = async () => { - const currentSpell = getCurrentSpell() + // const currentSpell = getCurrentSpell() // refetch spell from local DB to ensure it is the most up to date - const spellDoc = await getSpell(currentSpell.name) - const spell = spellDoc.toJSON() + const spell = { ...spellRef.current } const modules = await getSpellModules(spell) // attach modules to spell to be exported spell.modules = modules @@ -77,7 +93,7 @@ const EventHandler = ({ pubSub, tab }) => { const url = window.URL.createObjectURL(new Blob([blob])) const link = document.createElement('a') link.href = url - link.setAttribute('download', `${spellDoc.name}.thoth`) + link.setAttribute('download', `${spell.name}.thoth`) // Append to html link element page document.body.appendChild(link) @@ -110,7 +126,7 @@ const EventHandler = ({ pubSub, tab }) => { } useEffect(() => { - if (!tab) return + if (!tab && !spell) return const subscriptions = Object.entries(handlerMap).map(([event, handler]) => { return subscribe(event, handler) diff --git a/client/src/features/Thoth/components/Workspace.jsx b/client/src/features/Thoth/components/Workspace.jsx index 8a1cba99f..34adaa7c4 100644 --- a/client/src/features/Thoth/components/Workspace.jsx +++ b/client/src/features/Thoth/components/Workspace.jsx @@ -3,7 +3,10 @@ import { useEffect } from 'react' import { Editor, useEditor } from '../../../contexts/EditorProvider' import { Layout } from '../../../contexts/LayoutProvider' import { useModule } from '../../../contexts/ModuleProvider' -import { useSpell } from '../../../contexts/SpellProvider' +import { + useLazyGetSpellQuery, + useSaveSpellMutation, +} from '../../../state/spells' import WorkspaceProvider from '../../../contexts/WorkspaceProvider' import { debounce } from '../../../utils/debounce' import EditorWindow from './EditorWindow' @@ -14,7 +17,8 @@ import StateManager from './StateManagerWindow' import TextEditor from './TextEditorWindow' const Workspace = ({ tab, appPubSub }) => { - const { saveSpell, loadSpell } = useSpell() + const [loadSpell, { data: spellData }] = useLazyGetSpellQuery() + const [saveSpell] = useSaveSpellMutation() const { saveModule } = useModule() const { editor } = useEditor() @@ -24,8 +28,9 @@ const Workspace = ({ tab, appPubSub }) => { return editor.on( 'save nodecreated noderemoved connectioncreated connectionremoved nodetranslated', debounce(() => { - if (tab.type === 'spell') - saveSpell(tab.spell, { graph: editor.toJSON() }, false) + if (tab.type === 'spell') { + saveSpell({ ...spellData, graph: editor.toJSON() }, false) + } if (tab.type === 'module') { saveModule(tab.module, { data: editor.toJSON() }, false) } diff --git a/client/src/index.js b/client/src/index.js index 8b1ef234c..34ec55c85 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -2,16 +2,20 @@ import './wdyr' import 'regenerator-runtime/runtime' import React from 'react' import ReactDOM from 'react-dom' +import { Provider } from 'react-redux' import App from './App' import AppProviders from './contexts/AppProviders' import reportWebVitals from './reportWebVitals' +import { store } from './state/store' ReactDOM.render( - - - + + + + + , document.getElementById('root') ) diff --git a/client/src/state/hooks.ts b/client/src/state/hooks.ts new file mode 100644 index 000000000..b26693338 --- /dev/null +++ b/client/src/state/hooks.ts @@ -0,0 +1,6 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import type { RootState, AppDispatch } from './store' + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = () => useDispatch() +export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/client/src/state/spells.ts b/client/src/state/spells.ts new file mode 100644 index 000000000..d8a167fd3 --- /dev/null +++ b/client/src/state/spells.ts @@ -0,0 +1,92 @@ +import { createSelector } from '@reduxjs/toolkit' +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import { Spell as SpellType } from '@latitudegames/thoth-core/types' + +import { initDB } from '../database' + +const _spellModel = async () => { + const db = await initDB() + const { spells } = db.models + return spells +} +export interface Spell { + user?: Record | null | undefined + name: string + graph: SpellType + gameState: Record + createdAt?: number + updatedAt?: number +} + +export const spellApi = createApi({ + reducerPath: 'spellApi', + baseQuery: fetchBaseQuery({ + baseUrl: process.env.REACT_APP_API_URL || 'localhost:8000/', + }), + tagTypes: ['Spell'], + endpoints: builder => ({ + getSpells: builder.query({ + async queryFn() { + const spellModel = await _spellModel() + const spells = await spellModel.getSpells() + + return { data: spells.map(spell => spell.toJSON()) } + }, + providesTags: ['Spell'], + }), + getSpell: builder.query({ + async queryFn(spellId) { + const spellModel = await _spellModel() + const spell = await spellModel.getSpell(spellId) + + return { data: spell.toJSON() } + }, + }), + saveSpell: builder.mutation, Partial>({ + async queryFn(spell) { + const spellModel = await _spellModel() + const updatedSpell = await spellModel.saveSpell(spell.name, spell) + return { data: updatedSpell.toJSON() } + }, + invalidatesTags: ['Spell'], + }), + newSpell: builder.mutation>({ + async queryFn(spellData) { + const newSpell = { gameState: {}, ...spellData } + const spellModel = await _spellModel() + + const spell = await spellModel.newSpell(newSpell) + + return { data: spell.toJSON() } + }, + invalidatesTags: ['Spell'], + }), + }), +}) + +const selectSpellResults = spellApi.endpoints.getSpells.select() +const emptySpells = [] + +export const selectAllSpells = createSelector( + selectSpellResults, + spellResult => spellResult?.data || emptySpells +) + +export const selectSpellById = createSelector( + [selectAllSpells, (state, spellId) => spellId], + (spells, spellId) => + spells.find(spell => { + return spell.name === spellId + }) +) + +export const { + useGetSpellQuery, + useGetSpellsQuery, + useLazyGetSpellQuery, + useNewSpellMutation, + useSaveSpellMutation, +} = spellApi + +export const useGetSpellSubscription = + spellApi.endpoints.getSpell.useLazyQuerySubscription diff --git a/client/src/state/store.ts b/client/src/state/store.ts new file mode 100644 index 000000000..ba8fca672 --- /dev/null +++ b/client/src/state/store.ts @@ -0,0 +1,24 @@ +import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit' +import { setupListeners } from '@reduxjs/toolkit/query/react' +import tabReducer from './tabs' +import { spellApi } from './spells' + +export const store = configureStore({ + reducer: { + tabs: tabReducer, + [spellApi.reducerPath]: spellApi.reducer, + }, + middleware: getDefaultMiddleware => + getDefaultMiddleware().concat(spellApi.middleware), +}) + +setupListeners(store.dispatch) + +export type AppDispatch = typeof store.dispatch +export type RootState = ReturnType +export type AppThunk = ThunkAction< + ReturnType, + RootState, + unknown, + Action +> diff --git a/client/src/state/tabs.ts b/client/src/state/tabs.ts new file mode 100644 index 000000000..70bb88b32 --- /dev/null +++ b/client/src/state/tabs.ts @@ -0,0 +1,87 @@ +import { v4 as uuidv4 } from 'uuid' +import { + createSlice, + // PayloadAction, + createDraftSafeSelector, + createEntityAdapter, +} from '@reduxjs/toolkit' + +import { RootState } from './store' +import defaultJson from '../contexts/layouts/defaultLayout.json' + +// Used to set workspaces to tabs +const workspaceMap = { + default: defaultJson, +} +export interface Tab { + id: string + name: string + active: boolean + layoutJson: Record + type?: 'spell' | 'module' + // probably going to need to insert a proper spell type in here + spell?: string + // this will also be a ref to a property somewhere else + module: string +} + +// Entity adapter +const tabAdapater = createEntityAdapter() +const tabSelectors = tabAdapater.getSelectors() + +// Initial State +const initialState = tabAdapater.getInitialState() + +// Selectors +const _activeTabSelector = createDraftSafeSelector( + tabSelectors.selectAll, + tabs => { + return Object.values(tabs).find(tab => tab?.active) + } +) + +// Used to build a tab with various defaults set, as well as workspace json and UUID +const buildTab = (tab, properties = {}) => ({ + ...tab, + name: tab.name || 'Untitled', + id: uuidv4(), + layoutJson: workspaceMap[tab.workspace || 'default'], + spell: tab?.spell || null, + type: tab?.type || 'module', + module: tab?.moduleName || null, + ...properties, +}) + +// This is the primary composed of our "duck", and returns a number of helper functions and properties. +export const tabSlice = createSlice({ + name: 'tabs', + initialState, + reducers: { + tabOpened: (state, action) => { + const activeTab = _activeTabSelector(state) as Tab + if (activeTab) activeTab.active = false + + const tab = buildTab(action.payload, { active: true }) + tabAdapater.addOne(state, tab) + }, + tabClosed: tabAdapater.removeOne, + tabSwitched: tabAdapater.updateOne, + tabsCleared: tabAdapater.removeAll, + tabLayoutSaved: () => {}, + }, +}) + +// actions +export const { + tabOpened, + tabClosed, + tabSwitched, + tabsCleared, + tabLayoutSaved, +} = tabSlice.actions + +// selectors +export const activeTabSelector = (state: RootState) => + _activeTabSelector(state.tabs) + +export default tabSlice.reducer diff --git a/core/src/editor.ts b/core/src/editor.ts index 66015e55f..a7d152331 100644 --- a/core/src/editor.ts +++ b/core/src/editor.ts @@ -194,7 +194,8 @@ export const initEditor = async function ({ await engine.abort() } - editor.loadGraph = async (graph: Data) => { + editor.loadGraph = async (_graph: Data) => { + const graph = JSON.parse(JSON.stringify(_graph)) await engine.abort() editor.fromJSON(graph) editor.view.resize() diff --git a/yarn.lock b/yarn.lock index ca5a69509..7df62a4f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1039,7 +1039,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== @@ -2461,6 +2461,16 @@ dependencies: reflexbox "^4.0.6" +"@reduxjs/toolkit@^1.6.2": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37" + integrity sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA== + dependencies: + immer "^9.0.6" + redux "^4.1.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@rollup/plugin-babel@^5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" @@ -2789,6 +2799,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^5.0.0": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57" @@ -2981,6 +2999,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.16", "@types/react-redux@^7.1.18": + version "7.1.18" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.18.tgz#2bf8fd56ebaae679a90ebffe48ff73717c438e04" + integrity sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-transition-group@^4.2.0": version "4.4.3" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.3.tgz#b0994da0a7023d67dbb4a8910a62112bc00d5688" @@ -7796,6 +7824,11 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= +immer@^9.0.6: + version "9.0.6" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73" + integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ== + import-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92" @@ -11779,6 +11812,18 @@ react-pouchdb@^2.1.0: pouchdb-selector-core "^7.1.1" use-subscription "^1.1.1" +react-redux@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.5.tgz#213c1b05aa1187d9c940ddfc0b29450957f6a3b8" + integrity sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" + react-select@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81" @@ -12030,6 +12075,18 @@ reduce-flatten@^2.0.0: resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + +redux@^4.0.0, redux@^4.1.0, redux@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47" + integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -12208,6 +12265,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -12294,14 +12356,6 @@ rete-react-render-plugin@^0.2.1: resolved "https://registry.yarnpkg.com/rete-react-render-plugin/-/rete-react-render-plugin-0.2.1.tgz#71a6d73f18f850b85262563f678b40080a7b0e32" integrity sha512-2ZMXUP0v+EiejHVMqdrOmUwyDBHC2UDOJ/pFkElaZL1Kn/E40JZA5yzdBXi6ajYZI2DCzoW5ZBcA2Ihjtur8MQ== -"rete@git+https://github.com/latitudegames/rete.git#master": - version "1.4.5" - uid "24565a81a2bbcdd4cd73b0a725977550cc8ff988" - resolved "git+https://github.com/latitudegames/rete.git#24565a81a2bbcdd4cd73b0a725977550cc8ff988" - dependencies: - lodash "^4.17.21" - watch "^1.0.2" - "rete@https://github.com/latitudegames/rete.git#master": version "1.4.5" resolved "https://github.com/latitudegames/rete.git#24565a81a2bbcdd4cd73b0a725977550cc8ff988"