diff --git a/.changeset/config.json b/.changeset/config.json index 12e6119e..45f9da82 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -12,7 +12,7 @@ ], "linked": [], "access": "public", - "baseBranch": "main", + "baseBranch": "8.3/main", "updateInternalDependencies": "patch", "ignore": [], "privatePackages": { diff --git a/.changeset/cute-eyes-rescue.md b/.changeset/cute-eyes-rescue.md new file mode 100644 index 00000000..0ba3459c --- /dev/null +++ b/.changeset/cute-eyes-rescue.md @@ -0,0 +1,5 @@ +--- +'@embr-js/perspective-client': patch +--- + +Added `getChildStore` helper function. Allows traversing an `ElementStore` by a list of path components. diff --git a/.changeset/gentle-hornets-battle.md b/.changeset/gentle-hornets-battle.md new file mode 100644 index 00000000..3109e269 --- /dev/null +++ b/.changeset/gentle-hornets-battle.md @@ -0,0 +1,5 @@ +--- +'@embr-jvm/perspective-gateway': minor +--- + +`ThreadContext` now includes a `ComponentModel`. This `ComponentModel` is loaded from the `self` context of the Jython frame. diff --git a/.changeset/pretty-poems-do.md b/.changeset/pretty-poems-do.md new file mode 100644 index 00000000..932d4ce0 --- /dev/null +++ b/.changeset/pretty-poems-do.md @@ -0,0 +1,6 @@ +--- +'@embr-modules/periscope-web': minor +'@embr-modules/periscope': minor +--- + +`runJavaScript` functions now expose `perspective.context.component`. This is the `ComponentModel` of the component that made the `runJavaScript` function call. diff --git a/libraries/javascript/perspective-client/src/utils/getChildStore/getChildStore.ts b/libraries/javascript/perspective-client/src/utils/getChildStore/getChildStore.ts new file mode 100644 index 00000000..b2359b30 --- /dev/null +++ b/libraries/javascript/perspective-client/src/utils/getChildStore/getChildStore.ts @@ -0,0 +1,23 @@ +import { AbstractUIElementStore } from '@inductiveautomation/perspective-client' + +/** + * Get a child store given the address path. + * @param store + * @param path + */ +export function getChildStore( + store: AbstractUIElementStore | undefined, + path: number[] +): AbstractUIElementStore | undefined { + let current = store + + for (const index of path) { + if (!current?.children || current.children[index] === undefined) { + // Path cannot be fully resolved + return undefined + } + current = current.children[index] + } + + return current +} diff --git a/libraries/javascript/perspective-client/src/utils/getChildStore/index.ts b/libraries/javascript/perspective-client/src/utils/getChildStore/index.ts new file mode 100644 index 00000000..ba17566f --- /dev/null +++ b/libraries/javascript/perspective-client/src/utils/getChildStore/index.ts @@ -0,0 +1 @@ +export * from './getChildStore' diff --git a/libraries/javascript/perspective-client/src/utils/index.ts b/libraries/javascript/perspective-client/src/utils/index.ts index 1e59fefc..68cec4d8 100644 --- a/libraries/javascript/perspective-client/src/utils/index.ts +++ b/libraries/javascript/perspective-client/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from './getChildStore' export { default as getClientStore } from './getClientStore' export { default as getDesignerStore } from './getDesignerStore' export { default as waitForClientStore } from './waitForClientStore' diff --git a/libraries/perspective/gateway/src/main/kotlin/com/mussonindustrial/embr/perspective/gateway/model/ThreadContext.kt b/libraries/perspective/gateway/src/main/kotlin/com/mussonindustrial/embr/perspective/gateway/model/ThreadContext.kt index 18e30f50..d596378f 100644 --- a/libraries/perspective/gateway/src/main/kotlin/com/mussonindustrial/embr/perspective/gateway/model/ThreadContext.kt +++ b/libraries/perspective/gateway/src/main/kotlin/com/mussonindustrial/embr/perspective/gateway/model/ThreadContext.kt @@ -1,24 +1,36 @@ package com.mussonindustrial.embr.perspective.gateway.model import com.inductiveautomation.perspective.gateway.api.PerspectiveElement +import com.inductiveautomation.perspective.gateway.model.ComponentModel import com.inductiveautomation.perspective.gateway.model.PageModel import com.inductiveautomation.perspective.gateway.model.ViewModel +import com.inductiveautomation.perspective.gateway.script.ComponentModelScriptWrapper import com.inductiveautomation.perspective.gateway.session.InternalSession +import com.mussonindustrial.embr.common.reflect.getPrivateProperty import com.mussonindustrial.embr.perspective.gateway.model.ThreadContext.Companion.get import com.mussonindustrial.embr.perspective.gateway.model.ThreadContext.Companion.set import java.lang.ref.WeakReference +import org.python.core.Py +import org.python.core.PyString -class ThreadContext(view: ViewModel?, page: PageModel?, session: InternalSession?) { +class ThreadContext( + session: InternalSession?, + page: PageModel?, + view: ViewModel?, + component: ComponentModel?, +) { val view = WeakReference(view) val page = WeakReference(page) val session = WeakReference(session) + val component = WeakReference(component) companion object { fun get(): ThreadContext { return ThreadContext( - ViewModel.VIEW.get(), - PageModel.PAGE.get(), InternalSession.SESSION.get(), + PageModel.PAGE.get(), + ViewModel.VIEW.get(), + getComponentModel(), ) } @@ -27,6 +39,13 @@ class ThreadContext(view: ViewModel?, page: PageModel?, session: InternalSession PageModel.PAGE.set(threadContext.page.get()) InternalSession.SESSION.set(threadContext.session.get()) } + + fun getComponentModel(): ComponentModel? { + val wrapper = + Py.getFrame().locals.__getitem__(PyString("self")) as? ComponentModelScriptWrapper + ?: return null + return wrapper.getPrivateProperty("componentModel") as? ComponentModel + } } } @@ -43,8 +62,9 @@ fun withThreadContext(threadContext: ThreadContext, block: () -> Unit) { val PerspectiveElement.threadContext: ThreadContext get() { return ThreadContext( - this.view as? ViewModel, - this.page as? PageModel, this.session as? InternalSession, + this.page as? PageModel, + this.view as? ViewModel, + this as? ComponentModel, ) } diff --git a/modules/periscope/gateway/src/main/kotlin/com/mussonindustrial/ignition/embr/periscope/api/JavaScriptRunMsg.kt b/modules/periscope/gateway/src/main/kotlin/com/mussonindustrial/ignition/embr/periscope/api/JavaScriptRunMsg.kt index 9d3bb7e5..05be1452 100644 --- a/modules/periscope/gateway/src/main/kotlin/com/mussonindustrial/ignition/embr/periscope/api/JavaScriptRunMsg.kt +++ b/modules/periscope/gateway/src/main/kotlin/com/mussonindustrial/ignition/embr/periscope/api/JavaScriptRunMsg.kt @@ -37,6 +37,14 @@ class JavaScriptRunMsg( threadContext.page.get()?.let { add("page", JsonObject().apply { addProperty("id", it.id) }) } + threadContext.component.get()?.let { + add( + "component", + JsonObject().apply { + addProperty("componentAddressPath", it.componentAddressPath) + }, + ) + } }, ) } diff --git a/modules/periscope/web/src/extensions/runJavaScript.ts b/modules/periscope/web/src/extensions/runJavaScript.ts index da12aad4..9cb05938 100644 --- a/modules/periscope/web/src/extensions/runJavaScript.ts +++ b/modules/periscope/web/src/extensions/runJavaScript.ts @@ -1,5 +1,8 @@ -import { createScriptingGlobals } from '@embr-js/perspective-client' -import { toUserScript } from '@embr-js/utils' +import { + createScriptingGlobals, + getChildStore, +} from '@embr-js/perspective-client' +import { toUserScript, UserScriptParams } from '@embr-js/utils' import { ClientStore } from '@inductiveautomation/perspective-client' export const PROTOCOL = { @@ -8,11 +11,41 @@ export const PROTOCOL = { ERROR: 'periscope-js-error', } +type RunJavaScriptPayload = { + function: string + args: UserScriptParams + id: string + context: RunJavaScriptContext +} + +type RunJavaScriptContext = { + view?: { + id: string + mountPath: string + resourcePath: string + } + page?: { + id: string + } + component?: { + componentAddressPath: string + } +} + +function getChildPath(componentAddressPath?: string) { + return componentAddressPath?.split(':').map(Number) ?? [] +} + export function installRunJavaScript(clientStore: ClientStore) { const thisArg = clientStore clientStore.connection.handlers.set(PROTOCOL.RUN, (payload) => { - const { function: functionLiteral, args, id } = payload + const { + function: functionLiteral, + args, + id, + context, + } = payload as RunJavaScriptPayload function resolveSuccess(data: unknown) { clientStore.connection.send(PROTOCOL.RESOLVE, { @@ -42,7 +75,21 @@ export function installRunJavaScript(clientStore: ClientStore) { } new Promise((resolve) => { - const globals = createScriptingGlobals({}) + const view = clientStore.page.findView( + context.view?.resourcePath ?? '', + context.view?.mountPath ?? '' + ) + + const componentPath = getChildPath( + context.component?.componentAddressPath + ) + + const globals = createScriptingGlobals({ + client: clientStore, + page: clientStore.page, + view, + component: getChildStore(view, componentPath), + }) const f = toUserScript(functionLiteral, thisArg, globals) resolve(f.runNamed(args))