diff --git a/modules/context/index.js b/modules/context/index.js deleted file mode 100644 index 8c6309e1..00000000 --- a/modules/context/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import Bus from '../bus'; - -export default { - services: {}, - streams: {}, - //@deprecated - bus: new Bus() -}; \ No newline at end of file diff --git a/modules/context/index.ts b/modules/context/index.ts new file mode 100644 index 00000000..149d02dd --- /dev/null +++ b/modules/context/index.ts @@ -0,0 +1,13 @@ +export interface ApplicationContext { + + services: any, + streams: any, +} + +export default { + + services: {}, + streams: {} + +} as ApplicationContext; + diff --git a/modules/doc/DocumentationWindow.tsx b/modules/doc/DocumentationWindow.tsx index 0a6ec32b..56e070c9 100644 --- a/modules/doc/DocumentationWindow.tsx +++ b/modules/doc/DocumentationWindow.tsx @@ -21,6 +21,7 @@ export function DocumentationWindow() { const [content, setContent] = useState(null); useEffect(() => { + // @ts-ignore window.__CAD_APP.DocumentationTopic$ = DocumentationTopic$; if (!request) { setContent(null); @@ -76,6 +77,7 @@ export function DocumentationWindow() { } DocumentationUIState$.throttle(3000).attach(el => { + // @ts-ignore const rect = el.getBoundingClientRect(); sessionStorage.setItem('DocumentationWindow', JSON.stringify({ width: rect.width, diff --git a/modules/lstream/index.d.ts b/modules/lstream/index.d.ts index f42cda90..e3af16eb 100644 --- a/modules/lstream/index.d.ts +++ b/modules/lstream/index.d.ts @@ -1,6 +1,9 @@ -interface StreamBase { +interface Observable { attach(callback: (value: T) => any): () => void +} + +interface Stream extends Observable { map(fn: (value: T) => V); @@ -19,23 +22,25 @@ interface StreamBase { pipe(otherStream): () => void; } -interface Stream extends StreamBase { - - -} - -interface StateStream extends Stream { - - value: T; +interface Emitter extends Stream { next(value?: T) : void; } +interface StateStream extends Emitter { -export function stream(): Stream; + value: T; -export function eventStream(): Stream; + update(updater: (T) => T): void; + + mutate(mutator: (T) => void): void; +} + + +export function stream(): Emitter; + +export function eventStream(): Emitter; export function combine(...streams: Stream[]): Stream; diff --git a/modules/lstream/throttle.js b/modules/lstream/throttle.js index c5e1368d..91d9105e 100644 --- a/modules/lstream/throttle.js +++ b/modules/lstream/throttle.js @@ -1,5 +1,4 @@ import {StreamBase} from './base'; -import {Emitter} from './emitter'; export class ThrottleStream extends StreamBase { @@ -13,7 +12,7 @@ export class ThrottleStream extends StreamBase { attach(observer) { let scheduled = false; let value = undefined; - this.stream.attach(val => { + return this.stream.attach(val => { value = this.accumulator(val); if (!scheduled) { setTimeout(() => { diff --git a/modules/ui/components/Menu.jsx b/modules/ui/components/Menu.jsx index 092b96b2..bf571614 100644 --- a/modules/ui/components/Menu.jsx +++ b/modules/ui/components/Menu.jsx @@ -1,10 +1,12 @@ -import React from 'react'; +import React, {useContext, useEffect, useState} from 'react'; import PropTypes from 'prop-types'; import ls from './Menu.less'; import AuxWidget from "./AuxWidget"; import cx from 'classnames'; import Fa from './Fa'; +import {UISystemContext} from "../../../web/app/cad/dom/components/UISystem"; +import {useStream} from "../effects"; export default function Menu({children, x, y, orientationUp, centered, menuId, ...props}) { return ; } -export class ContextMenu extends React.Component { +const ContextMenuContext = React.createContext(null); - state = { +export function ContextMenu(props) { + + const [state, setState] = useState({ active: false - }; + }); - onClick = e => { + const {onCloseAll} = useContext(UISystemContext); + + useEffect(() => onCloseAll.attach(close)); + + const onClick = e => { e.preventDefault(); - this.setState({ + setState({ active: true, x: e.clientX, y: e.clientY }); }; - close = () => { - this.setState({active: false}) + const close = () => { + setState({active: false}) }; - - componentDidMount() { - this.detacher = this.context.onCloseAll.attach(this.close); - } - componentWillUnmount() { - this.detacher(); - } - - render() { - return - {this.props.children} - - {this.state.active && - {this.props.items} + return + + {props.children} + + {state.active && + {props.items} } - } - - getChildContext() { - return { - closeMenu: this.close - }; - } - - static contextTypes = { - onCloseAll: PropTypes.object - }; - - static childContextTypes = { - closeMenu: PropTypes.func - }; + ; } -export function ContextMenuItem({onClick, ...props}, {closeMenu}) { +export function ContextMenuItem({onClick, ...props}) { + const closeMenu = useContext(ContextMenuContext); return { closeMenu(); onClick(); }} {...props}/>; } - -ContextMenuItem.contextTypes = { - closeMenu: PropTypes.func -}; - -MenuItem.contextTypes = { - closeAllUpPopups: PropTypes.func -}; diff --git a/modules/ui/connectLegacy.jsx b/modules/ui/connectLegacy.jsx deleted file mode 100644 index 2fafc7ba..00000000 --- a/modules/ui/connectLegacy.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export const PROPAGATE_SELF_PROPS = v => v; - -export default function connect(WrappedComponent, tokens, config) { - if (!config) { - config = DEFAULT_CONFIG; - } - let {staticProps, mapProps, mapActions, mapSelfProps} = config; - - mapProps = createMapper(mapProps); - - mapActions = mapActions || function({dispatch}) { - return dispatch; - }; - - mapSelfProps = PROPAGATE_SELF_PROPS; - - return class StateConnector extends React.PureComponent { - - constructor(props, {bus}) { - super(); - this.mounted = false; - this.stateProps = {}; - this.dispatchProps = mapActions(bus.externalAPI, props); - } - - UNSAFE_componentWillMount() { - this.externalStateConnection = this.context.bus.connectToState(this.getTokens(), this.setExternalState); - this.externalStateConnection(); - } - - getTokens() { - let tokensArr = tokens instanceof Function ? tokens(this.props) : tokens; - if (!Array.isArray(tokensArr)) { - tokensArr = [tokensArr]; - } - return tokensArr; - } - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - this.context.bus.disconnectFromState(this.externalStateConnection); - } - - setExternalState = (state) => { - this.stateProps = mapProps(state, this.props); - if (this.mounted) { - this.forceUpdate(); - } - }; - - render() { - return - } - - static contextTypes = { - bus: PropTypes.object - }; - }; -} - -function createMapper(mapper, comp) { - if (!mapper) { - return DEFAULT_MAPPER; - } else if (Array.isArray(mapper)) { - return function (state) { - let props = {}; - for (let i = 0; i < state.length; i++) { - let stateItem = state[i]; - let mapperItem = mapper[i]; - Object.assign(props, mapperItem ? mapperItem(stateItem) : stateItem) - } - return props; - }; - } - return mapper; -} - -const DEFAULT_CONFIG = {}; - -export function DEFAULT_MAPPER(state) { - let props = {}; - state.forEach(stateItem => Object.assign(props, stateItem)); - return props; -} diff --git a/web/app/brep/debug/debugger/BrepDebuggerWindow.jsx b/web/app/brep/debug/debugger/BrepDebuggerWindow.jsx index 456c1ace..8da571a6 100644 --- a/web/app/brep/debug/debugger/BrepDebuggerWindow.jsx +++ b/web/app/brep/debug/debugger/BrepDebuggerWindow.jsx @@ -1,15 +1,11 @@ import React from 'react'; import Window from 'ui/components/Window'; import BrepDebugger from './brepDebugger'; -import connect, {PROPAGATE_SELF_PROPS} from 'ui/connectLegacy'; import {addToGroup, clearGroup, createGroup, removeFromGroup} from 'scene/sceneGraph'; -import {createToken} from 'bus'; import Fa from 'ui/components/Fa'; import ls from './BrepDebuggerWindow.less'; -export const BREP_DEBUG_WINDOW_VISIBLE = createToken('debug', 'brepDebugWindowVisible') - class BrepDebuggerWindow extends React.Component { UNSAFE_componentWillMount() { @@ -35,10 +31,3 @@ class BrepDebuggerWindow extends React.Component { ; } } - -export default connect(BrepDebuggerWindow, BREP_DEBUG_WINDOW_VISIBLE, { - mapProps: ([visible]) => ({visible}), - mapActions: ({dispatch}) => ({ - close: () => dispatch(BREP_DEBUG_WINDOW_VISIBLE, false) - }) -}); \ No newline at end of file diff --git a/web/app/cad/debugPlugin.js b/web/app/cad/debugPlugin.js index 07e9f0ec..1e01df96 100644 --- a/web/app/cad/debugPlugin.js +++ b/web/app/cad/debugPlugin.js @@ -7,13 +7,13 @@ import * as vec from '../math/vec'; import React from 'react'; import {readSketchFloat} from './sketch/sketchReader'; import {toLoops} from '../brep/io/brepLoopsFormat'; -import {contributeComponent} from './dom/components/ContributedComponents'; -import BrepDebuggerWindow, {BREP_DEBUG_WINDOW_VISIBLE} from '../brep/debug/debugger/BrepDebuggerWindow'; import curveTess from '../brep/geom/impl/curve/curve-tess'; import {LOG_FLAGS} from './logFlags'; +import {state} from "lstream"; +const BREP_DEBUG_WINDOW_VISIBLE$ = state(false); -export function activate({bus, services, streams}) { +export function activate({services, streams}) { addGlobalDebugActions(services); addDebugSelectors(services); services.action.registerActions(DebugActions); @@ -24,8 +24,7 @@ export function activate({bus, services, streams}) { }; streams.ui.controlBars.left.update(actions => [...actions, 'menu.debug']); - bus.enableState(BREP_DEBUG_WINDOW_VISIBLE, false); - contributeComponent(); + // contributeComponent(); } function addGlobalDebugActions({viewer, cadScene, cadRegistry}) { @@ -434,7 +433,7 @@ const DebugActions = [ info: 'open the BREP debugger in a window', }, invoke: ({bus}) => { - bus.dispatch(BREP_DEBUG_WINDOW_VISIBLE, true); + BREP_DEBUG_WINDOW_VISIBLE$.next(true); } } diff --git a/web/app/cad/dom/appTabsPlugin.js b/web/app/cad/dom/appTabsPlugin.js deleted file mode 100644 index d4236f63..00000000 --- a/web/app/cad/dom/appTabsPlugin.js +++ /dev/null @@ -1,62 +0,0 @@ -import {createToken} from 'bus'; - -export function activate({services, bus}) { - bus.enableState(TOKENS.TABS, { - tabs: [], - activeTab: -1 - }); - - let detachedViews = {}; - - function show(id, label, url) { - let index = bus.state[TOKENS.TABS].tabs.findIndex(tab => tab.id === id); - if (index === -1) { - let detachedView = detachedViews[id]; - if (detachedView !== undefined) { - if (!detachedView.closed) { - detachedView.focus(); - return; - } else { - delete detachedViews[id]; - } - } - } - bus.updateState(TOKENS.TABS, ({tabs, activeTab}) => { - if (index === -1) { - return { - activeTab: tabs.length, - tabs: [...tabs, { - id, label, url - }], - } - } else { - return { - tabs, - activeTab: index - } - } - }); - } - - bus.subscribe(TOKENS.DETACH, index => { - let {id, url} = bus.state[TOKENS.TABS].tabs[index]; - detachedViews[id] = window.open(url, id, "height=900,width=1200") - bus.updateState(TOKENS.TABS, ({tabs}) => { - tabs.splice(index, 1); - return { - tabs, - activeTab: -1 - } - }) - }); - - services.appTabs = { - show - } -} - -export const TOKENS = { - TABS: createToken('appTabs', 'tabs'), - OPEN: createToken('appTabs', 'open'), - DETACH: createToken('appTabs', 'detach') -}; \ No newline at end of file diff --git a/web/app/cad/dom/appTabsPlugin.ts b/web/app/cad/dom/appTabsPlugin.ts new file mode 100644 index 00000000..8b56ddf9 --- /dev/null +++ b/web/app/cad/dom/appTabsPlugin.ts @@ -0,0 +1,86 @@ +import {state, StateStream} from "lstream"; +import {ApplicationContext} from "context"; + +export function activate(ctx: ApplicationContext) { + + const tabs$ = state({ + tabs: [] as AppTab[], + activeTab: -1 + }); + + const detachedViews = {}; + + function show(id, label, url) { + let index = tabs$.value.tabs.findIndex(tab => tab.id === id); + if (index === -1) { + let detachedView = detachedViews[id]; + if (detachedView !== undefined) { + if (!detachedView.closed) { + detachedView.focus(); + return; + } else { + delete detachedViews[id]; + } + } + } + tabs$.update(({tabs, activeTab}) => { + if (index === -1) { + return { + activeTab: tabs.length, + tabs: [...tabs, { + id, label, url + }], + } + } else { + return { + tabs, + activeTab: index + } + } + }); + } + + function detach(index) { + let {id, url} = tabs$.value.tabs[index]; + detachedViews[id] = window.open(url, id, "height=900,width=1200") + tabs$.update(({tabs}) => { + tabs.splice(index, 1); + return { + tabs, + activeTab: -1 + } + }) + } + + ctx.appTabsService = { + show, detach, tabs$ + } +} + +export interface AppTab { + id: string; + label: string; + url: string; +} + +export interface AppTabsState { + tabs: AppTab[]; + activeTab: number; +} + +export interface AppTabsService { + + tabs$: StateStream; + + show(id:string, label:string, url:string); + + detach(index: number) +} + +declare module 'context' { + interface ApplicationContext { + + appTabsService: AppTabsService; + } +} + diff --git a/web/app/cad/dom/components/AppContext.js b/web/app/cad/dom/components/AppContext.js deleted file mode 100644 index 433a29b3..00000000 --- a/web/app/cad/dom/components/AppContext.js +++ /dev/null @@ -1,3 +0,0 @@ -import React from "react"; - -export const AppContext = React.createContext({}); \ No newline at end of file diff --git a/web/app/cad/dom/components/AppContext.ts b/web/app/cad/dom/components/AppContext.ts new file mode 100644 index 00000000..fcf530d0 --- /dev/null +++ b/web/app/cad/dom/components/AppContext.ts @@ -0,0 +1,4 @@ +import React from "react"; +import {ApplicationContext} from "context"; + +export const AppContext: React.Context = React.createContext(null); \ No newline at end of file diff --git a/web/app/cad/dom/components/AppTabs.jsx b/web/app/cad/dom/components/AppTabs.jsx index 10dd0145..4907f4e9 100644 --- a/web/app/cad/dom/components/AppTabs.jsx +++ b/web/app/cad/dom/components/AppTabs.jsx @@ -1,16 +1,31 @@ -import React, {Fragment} from 'react'; +import React, {useContext} from 'react'; import View3d from './View3d'; import ls from './AppTabs.less'; import TabSwitcher, {Tab} from 'ui/components/TabSwticher'; -import connect from 'ui/connectLegacy'; - -import {TOKENS as APP_TABS_TOKENS} from "../appTabsPlugin"; import Card from "ui/components/Card"; +import {useStreamWithUpdater} from "../../../../../modules/ui/effects"; +import {AppContext} from "./AppContext"; + +export default function AppTabs({}) { + + const [{tabs, activeTab}, updateTabs] = useStreamWithUpdater(ctx => ctx.appTabsService.tabs$); + const ctx = useContext(AppContext); + + const switchTo = index => updateTabs(({tabs}) => ({tabs, activeTab: index})); + + const close = index => updateTabs(({activeTab, tabs}) => { + tabs.splice(index, 1); + return { + activeTab: (activeTab === index ? -1 : activeTab), + tabs + }; + }); + + const detach = index => ctx.detach(index); -function AppTabs({activeTab, tabs, switchTo, close, detach}) { return
@@ -44,20 +59,6 @@ function AppTabs({activeTab, tabs, switchTo, close, detach}) {
} -export default connect(AppTabs, APP_TABS_TOKENS.TABS, { - mapActions: ({dispatch, updateState}) => ({ - switchTo: index => updateState(APP_TABS_TOKENS.TABS, ({tabs}) => ({tabs, activeTab: index})), - close: index => updateState(APP_TABS_TOKENS.TABS, ({activeTab, tabs}) => { - tabs.splice(index, 1); - return { - activeTab: (activeTab === index ? -1 : activeTab), - tabs - }; - }), - detach: index => dispatch(APP_TABS_TOKENS.DETACH, index) - }) -}); - export function FrameView({url}) { return