From 76d2f475f575c3077010ffd336dae41cb3c797fd Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Sat, 1 Jul 2023 20:41:57 +0100 Subject: [PATCH 01/35] experiments(dui3): coupled with some other rhino experiments that i need to push still, investigating some patterns re composables & co. in nuxt and possibly having a strategy around multiple host app bindings --- packages/dui3/components/header/NavBar.vue | 4 +- .../dui3/lib/accounts/composables/setup.ts | 69 +++++++++++++++++++ packages/dui3/nuxt.config.ts | 2 +- packages/dui3/pages/index.vue | 47 ++++++++++--- packages/dui3/plugins/00.cefPlugin.ts | 26 +++++++ packages/dui3/plugins/apollo.ts | 53 +++++++------- packages/dui3/types/index.ts | 40 +++++++++++ 7 files changed, 201 insertions(+), 40 deletions(-) create mode 100644 packages/dui3/lib/accounts/composables/setup.ts create mode 100644 packages/dui3/plugins/00.cefPlugin.ts create mode 100644 packages/dui3/types/index.ts diff --git a/packages/dui3/components/header/NavBar.vue b/packages/dui3/components/header/NavBar.vue index f7412309c..bd5f0ac92 100644 --- a/packages/dui3/components/header/NavBar.vue +++ b/packages/dui3/components/header/NavBar.vue @@ -7,12 +7,12 @@
-
diff --git a/packages/dui3/lib/accounts/composables/setup.ts b/packages/dui3/lib/accounts/composables/setup.ts new file mode 100644 index 000000000..275dc4757 --- /dev/null +++ b/packages/dui3/lib/accounts/composables/setup.ts @@ -0,0 +1,69 @@ +import { ApolloClient } from '@apollo/client/core' +import { ApolloClients } from '@vue/apollo-composable' +import { ShallowRef } from 'vue' +import { resolveClientConfig } from '~/lib/core/configs/apollo' +import { Account } from '~/types' + +export type DUIAccount = { + accountInfo: Account + client: ApolloClient +} + +export type DUIAccountsState = { + accounts: ShallowRef + refreshAccounts: () => Promise +} + +const AccountsInjectionKey = 'DUI_ACCOUNTS_STATE' + +export async function useAccountsSetup() { + const app = useNuxtApp() + const $bindings = app.$bindings + + // Using a shallow ref as we don't need inner values reactive + const accounts = shallowRef([] as DUIAccount[]) + + const apolloClients = {} as Record> + + // Matches local accounts coming from the host app to app state. + const refreshAccounts = async () => { + const accs = JSON.parse(await $bindings.getAccounts()) as Account[] + const newAccs = [] as DUIAccount[] + for (const acc of accs) { + const existing = accounts.value.find((a) => a.accountInfo.id === acc.id) + if (existing) { + newAccs.push(existing) + continue + } + + const client = new ApolloClient( + resolveClientConfig({ + httpEndpoint: new URL('/graphql', acc.serverInfo.url).href, + authToken: () => acc.token + }) + ) + apolloClients[acc.id] = client + newAccs.push({ + accountInfo: acc, + client + }) + } + accounts.value = newAccs + } + + await refreshAccounts() + + const accState = { + accounts, + refreshAccounts + } + + app.vueApp.provide(ApolloClients, apolloClients) + provide(AccountsInjectionKey, accState) + return accState +} + +export function useInjectedAccounts() { + const state = inject(AccountsInjectionKey) as DUIAccountsState + return state +} diff --git a/packages/dui3/nuxt.config.ts b/packages/dui3/nuxt.config.ts index f3658e0ce..a18ebaa86 100644 --- a/packages/dui3/nuxt.config.ts +++ b/packages/dui3/nuxt.config.ts @@ -6,7 +6,7 @@ export default defineNuxtConfig({ shim: false, strict: true }, - modules: ['@nuxtjs/tailwindcss'], + modules: ['@nuxtjs/tailwindcss', '@speckle/ui-components-nuxt'], alias: { // Rewriting all lodash calls to lodash-es for proper tree-shaking & chunk splitting lodash: 'lodash-es' diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index 67447869e..9e367c6f1 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -1,22 +1,45 @@ diff --git a/packages/dui3/plugins/00.cefPlugin.ts b/packages/dui3/plugins/00.cefPlugin.ts new file mode 100644 index 000000000..adf676f38 --- /dev/null +++ b/packages/dui3/plugins/00.cefPlugin.ts @@ -0,0 +1,26 @@ +import { ICefSharp, WebUiBindingType, MockedBindings } from '~/types' + +declare let CefSharp: ICefSharp +declare let WebUIBinding: WebUiBindingType + +export default defineNuxtPlugin(async () => { + let bindings: WebUiBindingType + + try { + if (!CefSharp) throw new Error('No global CefSharp object found.') + await CefSharp.BindObjectAsync('WebUIBinding') + console.info('Bound WebUIBinding object for CefSharp.') + bindings = WebUIBinding + } catch (e) { + console.error('Failed to bind CefSharp, will use mocked bindings.') + console.error(e) + + bindings = MockedBindings + } + + return { + provide: { + bindings + } + } +}) diff --git a/packages/dui3/plugins/apollo.ts b/packages/dui3/plugins/apollo.ts index 104300198..ddf4dcd77 100644 --- a/packages/dui3/plugins/apollo.ts +++ b/packages/dui3/plugins/apollo.ts @@ -1,34 +1,33 @@ -import { ApolloClient } from '@apollo/client/core' -import { ApolloClients } from '@vue/apollo-composable' -import { resolveClientConfig } from '~/lib/core/configs/apollo' +// import { ApolloClient } from '@apollo/client/core' +// import { ApolloClients } from '@vue/apollo-composable' +// import { resolveClientConfig } from '~/lib/core/configs/apollo' export default defineNuxtPlugin((nuxtApp) => { /** * TODO: You can use `window` here to get credentials for all of the clients * we need from the parent connectors. The following is just an example */ - - const apolloClients = { - latest: new ApolloClient( - // Imagine endpoint & token is resolved from window or something - resolveClientConfig({ - httpEndpoint: 'https://latest.speckle.systems/graphql', - authToken: () => null - }) - ), - xyz: new ApolloClient( - // Imagine endpoint & token is resolved from window or something - resolveClientConfig({ - httpEndpoint: 'https://speckle.xyz/graphql', - authToken: () => null - }) - ) - } - - nuxtApp.vueApp.provide(ApolloClients, apolloClients) - return { - provide: { - apolloClients - } - } + // const { $bindings } = useNuxtApp() + // const apolloClients = { + // latest: new ApolloClient( + // // Imagine endpoint & token is resolved from window or something + // resolveClientConfig({ + // httpEndpoint: 'https://latest.speckle.systems/graphql', + // authToken: () => null + // }) + // ), + // xyz: new ApolloClient( + // // Imagine endpoint & token is resolved from window or something + // resolveClientConfig({ + // httpEndpoint: 'https://speckle.xyz/graphql', + // authToken: () => null + // }) + // ) + // } + // nuxtApp.vueApp.provide(ApolloClients, apolloClients) + // return { + // provide: { + // apolloClients + // } + // } }) diff --git a/packages/dui3/types/index.ts b/packages/dui3/types/index.ts new file mode 100644 index 000000000..b637d3f75 --- /dev/null +++ b/packages/dui3/types/index.ts @@ -0,0 +1,40 @@ +/* eslint-disable @typescript-eslint/require-await */ +export type Account = { + id: string + isDefault: boolean + token: string + serverInfo: { + name: string + url: string + } + userInfo: { + id: string + avatar: string + email: string + name: string + commits: { totalCount: number } + streams: { totalCount: number } + } +} + +export interface ICefSharp { + BindObjectAsync: (arg: string) => Promise +} + +export type WebUiBindingType = { + getAccounts: () => Promise + sayHi: (name: string) => Promise + getSourceAppName: () => Promise +} + +export const MockedBindings: WebUiBindingType = { + async getAccounts() { + return '[]' + }, + async sayHi(name: string) { + return `Hi ${name} from (mocked bindings)!` + }, + async getSourceAppName() { + return 'Mocked App' + } +} From 97add85d2e81748e53c5ca30e727df74dfe4685c Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Thu, 6 Jul 2023 18:02:34 +0100 Subject: [PATCH 02/35] experiments(dui3): super wip work --- packages/dui3/components/header/NavBar.vue | 7 ++- .../dui3/lib/accounts/composables/setup.ts | 2 + packages/dui3/plugins/00.bindings.ts | 43 +++++++++++++ packages/dui3/plugins/00.cefPlugin.ts | 41 ++++++------ packages/dui3/plugins/apollo.ts | 63 ++++++++++--------- packages/dui3/types/index.ts | 12 ++++ 6 files changed, 117 insertions(+), 51 deletions(-) create mode 100644 packages/dui3/plugins/00.bindings.ts diff --git a/packages/dui3/components/header/NavBar.vue b/packages/dui3/components/header/NavBar.vue index bd5f0ac92..097e23e17 100644 --- a/packages/dui3/components/header/NavBar.vue +++ b/packages/dui3/components/header/NavBar.vue @@ -14,10 +14,15 @@ class="hidden md:inline-block" /> --> + + Show Dev Tools + - + diff --git a/packages/dui3/lib/accounts/composables/setup.ts b/packages/dui3/lib/accounts/composables/setup.ts index 275dc4757..3bf7aa86a 100644 --- a/packages/dui3/lib/accounts/composables/setup.ts +++ b/packages/dui3/lib/accounts/composables/setup.ts @@ -5,7 +5,9 @@ import { resolveClientConfig } from '~/lib/core/configs/apollo' import { Account } from '~/types' export type DUIAccount = { + /** account info coming from the host app */ accountInfo: Account + /** the graphql client; a bit superflous */ client: ApolloClient } diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts new file mode 100644 index 000000000..57b409f7f --- /dev/null +++ b/packages/dui3/plugins/00.bindings.ts @@ -0,0 +1,43 @@ +import { ICefSharp, IWebViev, WebUiBindingType, MockedBindings } from '~/types' + +declare let CefSharp: ICefSharp +declare let chrome: IWebViev +declare let WebUIBinding: WebUiBindingType + +export default defineNuxtPlugin(async () => { + let bindings: WebUiBindingType | undefined = undefined + + try { + if (!CefSharp) throw new Error('No global CefSharp object found.') + await CefSharp.BindObjectAsync('WebUIBinding') + console.info('Bound WebUIBinding object for CefSharp.') + bindings = WebUIBinding + } catch (e) { + console.warn('Failed to bind CefSharp.') + console.warn(e) + } + + try { + if (!chrome.webview) throw new Error('No global Webview2 object found.') + bindings = chrome.webview.hostObjects.WebUIBinding + console.info('Bound WebUIBinding object for Webview2.') + const res = await bindings.sayHi('Test') + console.log(res) + } catch (e) { + console.warn('Failed to bind Webview2.') + console.warn(e) + } + + // TODO: continue falling back for things like sketchup, rhino mac (which would use shitty url hacking scheme stuff) + + if (!bindings) { + console.warn('No bindings found - falling back to mocked bindings.') + bindings = MockedBindings + } + + return { + provide: { + bindings + } + } +}) diff --git a/packages/dui3/plugins/00.cefPlugin.ts b/packages/dui3/plugins/00.cefPlugin.ts index adf676f38..2f852fa86 100644 --- a/packages/dui3/plugins/00.cefPlugin.ts +++ b/packages/dui3/plugins/00.cefPlugin.ts @@ -1,26 +1,23 @@ -import { ICefSharp, WebUiBindingType, MockedBindings } from '~/types' +// import { ICefSharp, WebUiBindingType, MockedBindings } from '~/types' -declare let CefSharp: ICefSharp -declare let WebUIBinding: WebUiBindingType +// declare let CefSharp: ICefSharp +// declare let WebUIBinding: WebUiBindingType export default defineNuxtPlugin(async () => { - let bindings: WebUiBindingType - - try { - if (!CefSharp) throw new Error('No global CefSharp object found.') - await CefSharp.BindObjectAsync('WebUIBinding') - console.info('Bound WebUIBinding object for CefSharp.') - bindings = WebUIBinding - } catch (e) { - console.error('Failed to bind CefSharp, will use mocked bindings.') - console.error(e) - - bindings = MockedBindings - } - - return { - provide: { - bindings - } - } + // let bindings: WebUiBindingType + // try { + // if (!CefSharp) throw new Error('No global CefSharp object found.') + // await CefSharp.BindObjectAsync('WebUIBinding') + // console.info('Bound WebUIBinding object for CefSharp.') + // bindings = WebUIBinding + // } catch (e) { + // console.error('Failed to bind CefSharp, will use mocked bindings.') + // console.error(e) + // bindings = MockedBindings + // } + // return { + // provide: { + // bindings + // } + // } }) diff --git a/packages/dui3/plugins/apollo.ts b/packages/dui3/plugins/apollo.ts index ddf4dcd77..8f9a7dfb0 100644 --- a/packages/dui3/plugins/apollo.ts +++ b/packages/dui3/plugins/apollo.ts @@ -2,32 +2,39 @@ // import { ApolloClients } from '@vue/apollo-composable' // import { resolveClientConfig } from '~/lib/core/configs/apollo' -export default defineNuxtPlugin((nuxtApp) => { - /** - * TODO: You can use `window` here to get credentials for all of the clients - * we need from the parent connectors. The following is just an example - */ - // const { $bindings } = useNuxtApp() - // const apolloClients = { - // latest: new ApolloClient( - // // Imagine endpoint & token is resolved from window or something - // resolveClientConfig({ - // httpEndpoint: 'https://latest.speckle.systems/graphql', - // authToken: () => null - // }) - // ), - // xyz: new ApolloClient( - // // Imagine endpoint & token is resolved from window or something - // resolveClientConfig({ - // httpEndpoint: 'https://speckle.xyz/graphql', - // authToken: () => null - // }) - // ) - // } - // nuxtApp.vueApp.provide(ApolloClients, apolloClients) - // return { - // provide: { - // apolloClients - // } - // } +export default defineNuxtPlugin(() => { + // Note: as accounts can be refreshed at runtime, i tried as an experiment + // moving them to a composable (/lib/accounts/composables/setup.ts) }) + +// export default defineNuxtPlugin((nuxtApp) => { +// /** +// * TODO: You can use `window` here to get credentials for all of the clients +// * we need from the parent connectors. The following is just an example +// */ +// const { $bindings } = useNuxtApp() + +// const apolloClients = { +// latest: new ApolloClient( +// // Imagine endpoint & token is resolved from window or something +// resolveClientConfig({ +// httpEndpoint: 'https://latest.speckle.systems/graphql', +// authToken: () => null +// }) +// ), +// xyz: new ApolloClient( +// // Imagine endpoint & token is resolved from window or something +// resolveClientConfig({ +// httpEndpoint: 'https://speckle.xyz/graphql', +// authToken: () => null +// }) +// ) +// } + +// nuxtApp.vueApp.provide(ApolloClients, apolloClients) +// return { +// provide: { +// apolloClients +// } +// } +// }) diff --git a/packages/dui3/types/index.ts b/packages/dui3/types/index.ts index b637d3f75..6569247e7 100644 --- a/packages/dui3/types/index.ts +++ b/packages/dui3/types/index.ts @@ -21,10 +21,19 @@ export interface ICefSharp { BindObjectAsync: (arg: string) => Promise } +export interface IWebViev { + webview: { + hostObjects: { + WebUIBinding: WebUiBindingType + } + } +} + export type WebUiBindingType = { getAccounts: () => Promise sayHi: (name: string) => Promise getSourceAppName: () => Promise + openDevTools: () => Promise } export const MockedBindings: WebUiBindingType = { @@ -36,5 +45,8 @@ export const MockedBindings: WebUiBindingType = { }, async getSourceAppName() { return 'Mocked App' + }, + async openDevTools() { + console.log('Open it yourself. I am just a set of mocked bindings!!!') } } From 0923c3f1d29fc971a9475926b581748494edfd21 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Sun, 9 Jul 2023 19:00:20 +0100 Subject: [PATCH 03/35] experiments(dui3): super wip work --- packages/dui3/lib/bridge/webview.ts | 51 ++++++++++++++++++++++++++ packages/dui3/lib/sketchup/bindings.ts | 30 +++++++++++++++ packages/dui3/plugins/00.bindings.ts | 34 ++++++++++++++--- packages/dui3/types/index.ts | 43 +++++++++++++--------- 4 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 packages/dui3/lib/bridge/webview.ts create mode 100644 packages/dui3/lib/sketchup/bindings.ts diff --git a/packages/dui3/lib/bridge/webview.ts b/packages/dui3/lib/bridge/webview.ts new file mode 100644 index 000000000..d16d1c023 --- /dev/null +++ b/packages/dui3/lib/bridge/webview.ts @@ -0,0 +1,51 @@ +// github.com/johot/WebView2-better-bridge/blob/master/web-ui/src/betterBridge.ts + +type IWebView2 = { + webview: { + hostObjects: Record & { + sync: Record + } + } +} + +type IRawBridge = { + GetMethods: () => string[] + RunMethod: (methodName: string, args: string) => Promise +} + +declare let chrome: IWebView2 + +export class WebView2Bridge { + private webViewBridge: IRawBridge + + constructor(bridgeName: string) { + this.webViewBridge = chrome.webview.hostObjects[bridgeName] + + // NOTE: GetMethods is a call to the .NET side. + const availableMethodNames = + chrome.webview.hostObjects.sync[bridgeName].GetMethods() + + // NOTE: hoisting original calls as lowerCasedMethodNames, but using the UpperCasedName for the .NET call + // This allows us to follow js convetions and keep .NET ones too (eg. bindings.sayHi('') => public string SayHi(string name) {} + for (const methodName of availableMethodNames) { + const lowercasedMethodName = lowercaseMethodName(methodName) + const hoistTarget = this as unknown as Record + hoistTarget[lowercasedMethodName] = (...args: unknown[]) => + this.runMethod(methodName, args) + } + } + + private async runMethod(methodName: string, args: unknown[]): Promise { + const preserializedArgs = args.map((a) => JSON.stringify(a)) + // NOTE: RunMethod is a call to the .NET side. + const result = await this.webViewBridge.RunMethod( + methodName, + JSON.stringify(preserializedArgs) + ) + + return JSON.parse(result) as unknown + } +} + +const lowercaseMethodName = (name: string) => + name.charAt(0).toLowerCase() + name.slice(1) diff --git a/packages/dui3/lib/sketchup/bindings.ts b/packages/dui3/lib/sketchup/bindings.ts new file mode 100644 index 000000000..4cba29c33 --- /dev/null +++ b/packages/dui3/lib/sketchup/bindings.ts @@ -0,0 +1,30 @@ +import { IWebUiBinding } from '~/types' + +export class SketchupBindings implements IWebUiBinding { + requestId = 0 + requests = {} as Record + + SketchupBindings() { + // todo + } + + getAccounts() { + this.requestId++ + this.requests[this.requestId] = new Promise((resolve, reject) => resolve('[]')) + + return this.requests[this.requestId] as Promise + } + + async sayHi(name: string) { + return `Hi ${name} from (sketchup mocked bindings)!` + } + + getSourceAppName() { + return new Promise((resolve, reject) => resolve('sketchup')) as Promise + } + + async openDevTools() { + // eslint-disable-next-line no-alert + window.alert('Please right click and select show dev tools.') + } +} diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 57b409f7f..84b636630 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -1,11 +1,22 @@ -import { ICefSharp, IWebViev, WebUiBindingType, MockedBindings } from '~/types' +import { IWebUiBinding, MockedBindings } from '~/types' +import { WebView2Bridge } from '~/lib/bridge/webview' + +interface ICefSharp { + BindObjectAsync: (arg: string) => Promise +} + +interface IWebView2 { + webview: unknown +} declare let CefSharp: ICefSharp -declare let chrome: IWebViev -declare let WebUIBinding: WebUiBindingType +declare let chrome: IWebView2 +declare let sketchup: Record // + +declare let WebUIBinding: IWebUiBinding export default defineNuxtPlugin(async () => { - let bindings: WebUiBindingType | undefined = undefined + let bindings: IWebUiBinding | undefined = undefined try { if (!CefSharp) throw new Error('No global CefSharp object found.') @@ -19,22 +30,33 @@ export default defineNuxtPlugin(async () => { try { if (!chrome.webview) throw new Error('No global Webview2 object found.') - bindings = chrome.webview.hostObjects.WebUIBinding + bindings = new WebView2Bridge('WebUIBinding') as unknown as IWebUiBinding console.info('Bound WebUIBinding object for Webview2.') const res = await bindings.sayHi('Test') console.log(res) + + // TODO: Wrap the motherfucking wv2 bindings up } catch (e) { console.warn('Failed to bind Webview2.') console.warn(e) } - // TODO: continue falling back for things like sketchup, rhino mac (which would use shitty url hacking scheme stuff) + try { + if (!sketchup) throw new Error('No global sketchup object found.') + console.info('Found Sketchup. Hi SketchUp! We have yet... a lot of work to do :) ') + // TODO + } catch (e) { + console.warn('Failed to bind sketchup.') + console.warn(e) + } if (!bindings) { console.warn('No bindings found - falling back to mocked bindings.') bindings = MockedBindings } + ;(globalThis as Record).bindings = bindings + return { provide: { bindings diff --git a/packages/dui3/types/index.ts b/packages/dui3/types/index.ts index 6569247e7..47dc1807c 100644 --- a/packages/dui3/types/index.ts +++ b/packages/dui3/types/index.ts @@ -17,36 +17,45 @@ export type Account = { } } -export interface ICefSharp { - BindObjectAsync: (arg: string) => Promise +type FileState = { + models: ModelCard[] } -export interface IWebViev { - webview: { - hostObjects: { - WebUIBinding: WebUiBindingType - } - } +type ModelCard = { + serverUrl: string + modelId: string + projectId: string + type: 'sender' | 'receiver' + lastUpdatedAt: Date + // settings: Record??? + // report: Record??? + // progress: Record??? } -export type WebUiBindingType = { - getAccounts: () => Promise +export type IWebUiBinding = { sayHi: (name: string) => Promise - getSourceAppName: () => Promise openDevTools: () => Promise + getAccounts: () => Promise + getSourceAppName: () => Promise + // etc. + getFileState: () => Promise } -export const MockedBindings: WebUiBindingType = { - async getAccounts() { - return '[]' - }, +export const MockedBindings: IWebUiBinding = { async sayHi(name: string) { return `Hi ${name} from (mocked bindings)!` }, + async openDevTools() { + // eslint-disable-next-line no-alert + window.alert('Mocked bindings cannot do this. Sorry :(') + }, + async getAccounts() { + return [] + }, async getSourceAppName() { return 'Mocked App' }, - async openDevTools() { - console.log('Open it yourself. I am just a set of mocked bindings!!!') + async getFileState() { + return { models: [] } } } From ca63e2936a8518c033b45f732e3dc09c5a289cd4 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Sun, 9 Jul 2023 22:19:55 +0100 Subject: [PATCH 04/35] experiments(dui3): super wip work --- .../dui3/lib/accounts/composables/setup.ts | 8 +++- packages/dui3/lib/bridge/cefSharp.ts | 28 +++++++++++++ packages/dui3/lib/bridge/sketchup.ts | 1 + packages/dui3/lib/bridge/webview.ts | 14 +++++++ packages/dui3/lib/sketchup/bindings.ts | 30 -------------- packages/dui3/package.json | 1 + packages/dui3/pages/index.vue | 3 +- packages/dui3/plugins/00.bindings.ts | 16 ++++++-- packages/dui3/types/index.ts | 39 +++++++++++++++++-- 9 files changed, 101 insertions(+), 39 deletions(-) create mode 100644 packages/dui3/lib/bridge/cefSharp.ts create mode 100644 packages/dui3/lib/bridge/sketchup.ts delete mode 100644 packages/dui3/lib/sketchup/bindings.ts diff --git a/packages/dui3/lib/accounts/composables/setup.ts b/packages/dui3/lib/accounts/composables/setup.ts index 3bf7aa86a..d3c45e092 100644 --- a/packages/dui3/lib/accounts/composables/setup.ts +++ b/packages/dui3/lib/accounts/composables/setup.ts @@ -29,7 +29,7 @@ export async function useAccountsSetup() { // Matches local accounts coming from the host app to app state. const refreshAccounts = async () => { - const accs = JSON.parse(await $bindings.getAccounts()) as Account[] + const accs = await $bindings.getAccounts() const newAccs = [] as DUIAccount[] for (const acc of accs) { const existing = accounts.value.find((a) => a.accountInfo.id === acc.id) @@ -53,10 +53,16 @@ export async function useAccountsSetup() { accounts.value = newAccs } + // Call this one first to initialize the account state await refreshAccounts() + const defaultAccount = computed(() => + accounts.value.find((acc) => acc.accountInfo.isDefault) + ) + const accState = { accounts, + defaultAccount, refreshAccounts } diff --git a/packages/dui3/lib/bridge/cefSharp.ts b/packages/dui3/lib/bridge/cefSharp.ts new file mode 100644 index 000000000..2d480de24 --- /dev/null +++ b/packages/dui3/lib/bridge/cefSharp.ts @@ -0,0 +1,28 @@ +import { IWebUiBinding } from '~/types' +import { createNanoEvents, Emitter } from 'nanoevents' +import { HostAppEvents } from '~/types' + +export class CefSharpBridge { + private emitter: Emitter + + constructor(bindingObject: IWebUiBinding) { + const hoistTarget = this as unknown as Record + const hoistSource = bindingObject as unknown as Record + + for (const key in bindingObject) { + hoistTarget[key] = hoistSource[key] + } + + this.emitter = createNanoEvents() + this.emitter.emit('start', 'polo pasta') + } + + on(event: E, callback: HostAppEvents[E]) { + return this.emitter.on(event, callback) + } + + emit(eventName: string, payload: string) { + const parsedPayload = JSON.parse(payload) as unknown + this.emitter.emit(eventName, parsedPayload) + } +} diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts new file mode 100644 index 000000000..70b786d12 --- /dev/null +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -0,0 +1 @@ +// TODO diff --git a/packages/dui3/lib/bridge/webview.ts b/packages/dui3/lib/bridge/webview.ts index d16d1c023..0817c6edf 100644 --- a/packages/dui3/lib/bridge/webview.ts +++ b/packages/dui3/lib/bridge/webview.ts @@ -1,4 +1,6 @@ // github.com/johot/WebView2-better-bridge/blob/master/web-ui/src/betterBridge.ts +import { createNanoEvents, Emitter } from 'nanoevents' +import { HostAppEvents } from '~/types' type IWebView2 = { webview: { @@ -17,6 +19,7 @@ declare let chrome: IWebView2 export class WebView2Bridge { private webViewBridge: IRawBridge + private emitter: Emitter constructor(bridgeName: string) { this.webViewBridge = chrome.webview.hostObjects[bridgeName] @@ -33,6 +36,8 @@ export class WebView2Bridge { hoistTarget[lowercasedMethodName] = (...args: unknown[]) => this.runMethod(methodName, args) } + + this.emitter = createNanoEvents() } private async runMethod(methodName: string, args: unknown[]): Promise { @@ -45,6 +50,15 @@ export class WebView2Bridge { return JSON.parse(result) as unknown } + + on(event: E, callback: HostAppEvents[E]) { + return this.emitter.on(event, callback) + } + + emit(eventName: string, payload: string) { + const parsedPayload = JSON.parse(payload) as unknown + this.emitter.emit(eventName, parsedPayload) + } } const lowercaseMethodName = (name: string) => diff --git a/packages/dui3/lib/sketchup/bindings.ts b/packages/dui3/lib/sketchup/bindings.ts deleted file mode 100644 index 4cba29c33..000000000 --- a/packages/dui3/lib/sketchup/bindings.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { IWebUiBinding } from '~/types' - -export class SketchupBindings implements IWebUiBinding { - requestId = 0 - requests = {} as Record - - SketchupBindings() { - // todo - } - - getAccounts() { - this.requestId++ - this.requests[this.requestId] = new Promise((resolve, reject) => resolve('[]')) - - return this.requests[this.requestId] as Promise - } - - async sayHi(name: string) { - return `Hi ${name} from (sketchup mocked bindings)!` - } - - getSourceAppName() { - return new Promise((resolve, reject) => resolve('sketchup')) as Promise - } - - async openDevTools() { - // eslint-disable-next-line no-alert - window.alert('Please right click and select show dev tools.') - } -} diff --git a/packages/dui3/package.json b/packages/dui3/package.json index 6832566ad..012c5695a 100644 --- a/packages/dui3/package.json +++ b/packages/dui3/package.json @@ -34,6 +34,7 @@ "graphql": "^16.6.0", "graphql-tag": "^2.12.6", "lodash-es": "^4.17.21", + "nanoevents": "^8.0.0", "portal-vue": "^3.0.0", "subscriptions-transport-ws": "^0.11.0" }, diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index 9e367c6f1..2af7b8dbc 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -18,6 +18,7 @@ {{ acc.accountInfo.serverInfo.name }} +
Your default account is {{ defaultAccount?.accountInfo }}
{{ clientId }}: @@ -38,7 +39,7 @@ import { ServerInfoTestQuery } from '~/lib/common/generated/gql/graphql' const { $bindings } = useNuxtApp() const appName = await $bindings.getSourceAppName() -const { accounts, refreshAccounts } = await useAccountsSetup() +const { accounts, refreshAccounts, defaultAccount } = await useAccountsSetup() const versionQuery = graphql(` query ServerInfoTest { diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 84b636630..91d0547c7 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -1,5 +1,6 @@ import { IWebUiBinding, MockedBindings } from '~/types' import { WebView2Bridge } from '~/lib/bridge/webview' +import { CefSharpBridge } from '~/lib/bridge/cefSharp' interface ICefSharp { BindObjectAsync: (arg: string) => Promise @@ -11,10 +12,14 @@ interface IWebView2 { declare let CefSharp: ICefSharp declare let chrome: IWebView2 -declare let sketchup: Record // +declare let sketchup: Record declare let WebUIBinding: IWebUiBinding +// Tries to find the correct host application binding. The sequence is: +// - CEFSharp (.NET) +// - WebView2 (.NET) +// - Sketchup (Ruby) - NOT IMPLEMENTED export default defineNuxtPlugin(async () => { let bindings: IWebUiBinding | undefined = undefined @@ -22,7 +27,7 @@ export default defineNuxtPlugin(async () => { if (!CefSharp) throw new Error('No global CefSharp object found.') await CefSharp.BindObjectAsync('WebUIBinding') console.info('Bound WebUIBinding object for CefSharp.') - bindings = WebUIBinding + bindings = new CefSharpBridge(WebUIBinding) as unknown as IWebUiBinding } catch (e) { console.warn('Failed to bind CefSharp.') console.warn(e) @@ -32,10 +37,9 @@ export default defineNuxtPlugin(async () => { if (!chrome.webview) throw new Error('No global Webview2 object found.') bindings = new WebView2Bridge('WebUIBinding') as unknown as IWebUiBinding console.info('Bound WebUIBinding object for Webview2.') + const res = await bindings.sayHi('Test') console.log(res) - - // TODO: Wrap the motherfucking wv2 bindings up } catch (e) { console.warn('Failed to bind Webview2.') console.warn(e) @@ -57,6 +61,10 @@ export default defineNuxtPlugin(async () => { ;(globalThis as Record).bindings = bindings + bindings.on('test', (args) => { + console.log(args) + }) + return { provide: { bindings diff --git a/packages/dui3/types/index.ts b/packages/dui3/types/index.ts index 47dc1807c..29e43589b 100644 --- a/packages/dui3/types/index.ts +++ b/packages/dui3/types/index.ts @@ -1,3 +1,4 @@ +import { createNanoEvents, Emitter } from 'nanoevents' /* eslint-disable @typescript-eslint/require-await */ export type Account = { id: string @@ -30,17 +31,46 @@ type ModelCard = { // settings: Record??? // report: Record??? // progress: Record??? + status: 'idle' | 'inprogress' | 'error' | 'warning' | 'disabled' } -export type IWebUiBinding = { +type TestData = { + foo: number + bar: string + baz: boolean +} + +export interface HostAppEvents { + test: (data: TestData) => void + documentChanged: () => void + selectionChanged: () => void + documentClosed: () => void + updateModelCardState: () => void + displayToastNotification: () => void // bla bla bla +} + +export interface IWebUiBinding { sayHi: (name: string) => Promise openDevTools: () => Promise getAccounts: () => Promise getSourceAppName: () => Promise - // etc. - getFileState: () => Promise + // getFileState: () => Promise + // addModelCard(string modelId, string projectId), removeModelCard(...) // etc. etc. + /** + * Subscribe to messages from the host application. + * @param event + * @param callback + */ + on: (event: E, callback: HostAppEvents[E]) => void + /** + * Used by the host application to notify/send data to the web app. Do not use from the frontend. + * @param eventName + * @param args + */ + emit?: (eventName: string, args: Record) => void } +const mockedEmitter = createNanoEvents() export const MockedBindings: IWebUiBinding = { async sayHi(name: string) { return `Hi ${name} from (mocked bindings)!` @@ -57,5 +87,8 @@ export const MockedBindings: IWebUiBinding = { }, async getFileState() { return { models: [] } + }, + on(event: E, callback: HostAppEvents[E]) { + return mockedEmitter.on(event, callback) } } From 5f3b7a16f2445067c5ef7eeda868693d8ef2b1a8 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Mon, 10 Jul 2023 17:01:49 +0100 Subject: [PATCH 05/35] experiments(dui3): super wip work --- .../dui3/lib/accounts/composables/setup.ts | 10 ++- packages/dui3/lib/bridge/sketchup.ts | 85 +++++++++++++++++++ packages/dui3/pages/index.vue | 6 +- packages/dui3/plugins/00.bindings.ts | 16 +++- packages/dui3/types/index.ts | 9 +- 5 files changed, 112 insertions(+), 14 deletions(-) diff --git a/packages/dui3/lib/accounts/composables/setup.ts b/packages/dui3/lib/accounts/composables/setup.ts index d3c45e092..8e4a47e97 100644 --- a/packages/dui3/lib/accounts/composables/setup.ts +++ b/packages/dui3/lib/accounts/composables/setup.ts @@ -1,6 +1,6 @@ import { ApolloClient } from '@apollo/client/core' import { ApolloClients } from '@vue/apollo-composable' -import { ShallowRef } from 'vue' +import { ShallowRef, ComputedRef } from 'vue' import { resolveClientConfig } from '~/lib/core/configs/apollo' import { Account } from '~/types' @@ -14,15 +14,16 @@ export type DUIAccount = { export type DUIAccountsState = { accounts: ShallowRef refreshAccounts: () => Promise + defaultAccount: ComputedRef } const AccountsInjectionKey = 'DUI_ACCOUNTS_STATE' -export async function useAccountsSetup() { +export async function useAccountsSetup(): Promise { const app = useNuxtApp() const $bindings = app.$bindings - // Using a shallow ref as we don't need inner values reactive + // Using a shallow ref as we don't need inner values reactive (could be a needlessly big return from host app) const accounts = shallowRef([] as DUIAccount[]) const apolloClients = {} as Record> @@ -54,6 +55,7 @@ export async function useAccountsSetup() { } // Call this one first to initialize the account state + // QUESTION: could be flopped in a iife so as not to block and drop the asyncness of this setup function? await refreshAccounts() const defaultAccount = computed(() => @@ -71,7 +73,7 @@ export async function useAccountsSetup() { return accState } -export function useInjectedAccounts() { +export function useInjectedAccounts(): DUIAccountsState { const state = inject(AccountsInjectionKey) as DUIAccountsState return state } diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index 70b786d12..5d3fce19d 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -1 +1,86 @@ // TODO +import { rejects } from 'assert' +import { uniqueId } from 'lodash-es' +import { resolve } from 'path' + +declare let sketchup: { + exec: (data: Record) => void +} + +export class SketchupBridge { + private requests = {} as Record< + string, + { + resolve: (value: unknown) => void + reject: (reason: string | Error) => void + rejectTimerId: number + } + > + private bindingsName: string + private TIMEOUT_MS = 2000 // 2s + public isInitalized: Promise + private isInitializedResolved!: () => unknown + + constructor(bindingsName: string) { + // window.sketchup + this.bindingsName = bindingsName || 'default_bindings' + + sketchup.exec({ name: 'get_commands' }) + // Initialization continues in the receiveCommandsAndInitializeBridge function + + this.isInitalized = new Promise((resolve, reject) => { + // TODO + this.isInitializedResolved = resolve + }) + } + + // executeScript(...) from skp + private receiveCommandsAndInitializeBridge(commandNames: string[]) { + const hoistTarget = this as unknown as Record + + for (const commandName of commandNames) { + hoistTarget[commandName] = (...args: unknown[]) => + this.runMethod(commandName, args) + } + + // this.isInitalized = true + this.isInitializedResolved() + } + + private async runMethod(methodName: string, args: unknown[]): Promise { + const requestId = uniqueId(this.bindingsName) + + // The single exec way + sketchup.exec({ name: methodName, requestId, args }) + + return new Promise((resolve, reject) => { + this.requests[requestId] = { + resolve, + reject, + rejectTimerId: window.setTimeout(() => { + reject( + 'Sketchup response timed out - did not receive anything back in good time.' + ) + // TODO: clear request from requests object + }, this.TIMEOUT_MS) + } + }) + } + + private receiveResponse(requestId: string, data: string) { + // TODO + if (!this.requests[requestId]) return // throw new error? + const request = this.requests[requestId] + try { + // TODO: resolve also if data is null, it means it's a + // 'void' function call (does not return anything) + const parsedData = JSON.parse(data) as Record + request.resolve(parsedData) + } catch (e) { + request.reject(e as Error) + } finally { + window.clearTimeout(request.rejectTimerId) + delete this.requests[requestId] + } + } +} diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index 2af7b8dbc..09218cef4 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -50,11 +50,9 @@ const versionQuery = graphql(` `) watch(accounts, () => { - console.log('accs ref') + console.log('accounts were refreshed, shallow ref does its job') }) -/** - * Imagine these come from window or something - */ + const clientIds = accounts.value.map((a) => a.accountInfo.id) const queries: Record< diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 91d0547c7..c7407e75c 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -1,6 +1,7 @@ import { IWebUiBinding, MockedBindings } from '~/types' import { WebView2Bridge } from '~/lib/bridge/webview' import { CefSharpBridge } from '~/lib/bridge/cefSharp' +import { SketchupBridge } from '~/lib/bridge/sketchup' interface ICefSharp { BindObjectAsync: (arg: string) => Promise @@ -49,6 +50,10 @@ export default defineNuxtPlugin(async () => { if (!sketchup) throw new Error('No global sketchup object found.') console.info('Found Sketchup. Hi SketchUp! We have yet... a lot of work to do :) ') // TODO + const skpBindings = new SketchupBridge('default_bindings') + await skpBindings.isInitalized // resolve({...}) + + bindings = skpBindings as unknown as IWebUiBinding } catch (e) { console.warn('Failed to bind sketchup.') console.warn(e) @@ -61,9 +66,14 @@ export default defineNuxtPlugin(async () => { ;(globalThis as Record).bindings = bindings - bindings.on('test', (args) => { - console.log(args) - }) + // bindings.on('test', (args) => { + // console.log(args) + // }) + + // bindings.on('documentChanged', (args) => { + // // do somethings + // args.x + // }) return { provide: { diff --git a/packages/dui3/types/index.ts b/packages/dui3/types/index.ts index 29e43589b..d52617777 100644 --- a/packages/dui3/types/index.ts +++ b/packages/dui3/types/index.ts @@ -23,8 +23,8 @@ type FileState = { } type ModelCard = { - serverUrl: string - modelId: string + serverUrl: string // we need to select the correct account + modelId: string // we need to assemble the gql query properly projectId: string type: 'sender' | 'receiver' lastUpdatedAt: Date @@ -40,15 +40,17 @@ type TestData = { baz: boolean } +// .NET/Host App -> JS export interface HostAppEvents { test: (data: TestData) => void - documentChanged: () => void + documentChanged: (data: { x: number; y: boolean }) => void selectionChanged: () => void documentClosed: () => void updateModelCardState: () => void displayToastNotification: () => void // bla bla bla } +// JS -> asks for something form host app export interface IWebUiBinding { sayHi: (name: string) => Promise openDevTools: () => Promise @@ -56,6 +58,7 @@ export interface IWebUiBinding { getSourceAppName: () => Promise // getFileState: () => Promise // addModelCard(string modelId, string projectId), removeModelCard(...) // etc. etc. + /** * Subscribe to messages from the host application. * @param event From c452458812ee7c4cebbb669e4c70076bdf3145f1 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Mon, 10 Jul 2023 20:12:35 +0100 Subject: [PATCH 06/35] experiments(dui3): extra cleanup and comments around --- packages/dui3/lib/bridge/sketchup.ts | 54 ++++++++++++++++++---------- packages/dui3/plugins/00.bindings.ts | 33 ++++++++--------- packages/dui3/types/index.ts | 3 +- yarn.lock | 8 +++++ 4 files changed, 60 insertions(+), 38 deletions(-) diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index 5d3fce19d..7342a127b 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -1,12 +1,15 @@ // TODO -import { rejects } from 'assert' import { uniqueId } from 'lodash-es' -import { resolve } from 'path' declare let sketchup: { exec: (data: Record) => void } +/** + * This class operates in different way than the others, because calls into Sketchup are one way only. + * E.g., we cannot return values from internal calls to it (e.g., const test = sketchup.rubyCall() does not work ). + * Values are passed back + */ export class SketchupBridge { private requests = {} as Record< string, @@ -19,38 +22,51 @@ export class SketchupBridge { private bindingsName: string private TIMEOUT_MS = 2000 // 2s public isInitalized: Promise - private isInitializedResolved!: () => unknown + private resolveIsInitializedPromise!: () => unknown constructor(bindingsName: string) { - // window.sketchup this.bindingsName = bindingsName || 'default_bindings' + // Initialization continues in the receiveCommandsAndInitializeBridge function, + // where we expect sketchup to return to us the command names. sketchup.exec({ name: 'get_commands' }) - // Initialization continues in the receiveCommandsAndInitializeBridge function this.isInitalized = new Promise((resolve, reject) => { - // TODO - this.isInitializedResolved = resolve + this.resolveIsInitializedPromise = resolve + setTimeout( + () => + reject( + `Failed to get command names from Sketchup; timed out after ${this.TIMEOUT_MS}ms.` + ), + this.TIMEOUT_MS + ) }) } - // executeScript(...) from skp + /** + * Will be called by `executeScript('bindings.receiveCommandsAndInitializeBridge()')` from sketchup. This is where the hoisting happens. + * NOTE: Oguhzan, we can defintively have commandNames be a string, and not a string[] + * And do JSON.parse() here to get them out properly. + * @param commandNames + */ private receiveCommandsAndInitializeBridge(commandNames: string[]) { const hoistTarget = this as unknown as Record - for (const commandName of commandNames) { hoistTarget[commandName] = (...args: unknown[]) => this.runMethod(commandName, args) } - // this.isInitalized = true - this.isInitializedResolved() + this.resolveIsInitializedPromise() } + /** + * Internal calls to Sketchup. + * @param methodName + * @param args + */ private async runMethod(methodName: string, args: unknown[]): Promise { const requestId = uniqueId(this.bindingsName) - // The single exec way sketchup.exec({ name: methodName, requestId, args }) return new Promise((resolve, reject) => { @@ -59,22 +75,22 @@ export class SketchupBridge { reject, rejectTimerId: window.setTimeout(() => { reject( - 'Sketchup response timed out - did not receive anything back in good time.' + `Sketchup response timed out - did not receive anything back in good time (${this.TIMEOUT_MS}ms).` ) - // TODO: clear request from requests object + delete this.requests[requestId] }, this.TIMEOUT_MS) } }) } private receiveResponse(requestId: string, data: string) { - // TODO - if (!this.requests[requestId]) return // throw new error? + if (!this.requests[requestId]) + throw new Error( + `Sketchup Bridge found no request to resolve with the id of ${requestId}. Something is weird!` + ) const request = this.requests[requestId] try { - // TODO: resolve also if data is null, it means it's a - // 'void' function call (does not return anything) - const parsedData = JSON.parse(data) as Record + const parsedData = JSON.parse(data) as Record // TODO: check if data is undefined request.resolve(parsedData) } catch (e) { request.reject(e as Error) diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index c7407e75c..1eea200be 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -30,7 +30,9 @@ export default defineNuxtPlugin(async () => { console.info('Bound WebUIBinding object for CefSharp.') bindings = new CefSharpBridge(WebUIBinding) as unknown as IWebUiBinding } catch (e) { - console.warn('Failed to bind CefSharp.') + console.warn( + 'Failed to bind CefSharp. This can be totally normal if the host is different.' + ) console.warn(e) } @@ -38,24 +40,26 @@ export default defineNuxtPlugin(async () => { if (!chrome.webview) throw new Error('No global Webview2 object found.') bindings = new WebView2Bridge('WebUIBinding') as unknown as IWebUiBinding console.info('Bound WebUIBinding object for Webview2.') - - const res = await bindings.sayHi('Test') - console.log(res) } catch (e) { - console.warn('Failed to bind Webview2.') + console.warn( + 'Failed to bind Webview2. This can be totally normal if the host is different.' + ) console.warn(e) } try { if (!sketchup) throw new Error('No global sketchup object found.') console.info('Found Sketchup. Hi SketchUp! We have yet... a lot of work to do :) ') - // TODO - const skpBindings = new SketchupBridge('default_bindings') - await skpBindings.isInitalized // resolve({...}) + const skpBindings = new SketchupBridge('default_bindings') + // Note, because of the way Sketchup bindings work, we need to wait here + // for them to be fully initialized. + await skpBindings.isInitalized bindings = skpBindings as unknown as IWebUiBinding } catch (e) { - console.warn('Failed to bind sketchup.') + console.warn( + 'Failed to bind sketchup. This can be totally normal if the host is different.' + ) console.warn(e) } @@ -64,17 +68,10 @@ export default defineNuxtPlugin(async () => { bindings = MockedBindings } + // We need the bindings object in global scope to allow + // host applications to send messages back to it. ;(globalThis as Record).bindings = bindings - // bindings.on('test', (args) => { - // console.log(args) - // }) - - // bindings.on('documentChanged', (args) => { - // // do somethings - // args.x - // }) - return { provide: { bindings diff --git a/packages/dui3/types/index.ts b/packages/dui3/types/index.ts index d52617777..a7ecd09ab 100644 --- a/packages/dui3/types/index.ts +++ b/packages/dui3/types/index.ts @@ -1,5 +1,6 @@ -import { createNanoEvents, Emitter } from 'nanoevents' /* eslint-disable @typescript-eslint/require-await */ +import { createNanoEvents } from 'nanoevents' + export type Account = { id: string isDefault: boolean diff --git a/yarn.lock b/yarn.lock index a5d18fe64..9671dae3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10933,6 +10933,7 @@ __metadata: graphql: ^16.6.0 graphql-tag: ^2.12.6 lodash-es: ^4.17.21 + nanoevents: ^8.0.0 nuxt: ^3.5.0 portal-vue: ^3.0.0 postcss: ^8.4.18 @@ -32762,6 +32763,13 @@ __metadata: languageName: node linkType: hard +"nanoevents@npm:^8.0.0": + version: 8.0.0 + resolution: "nanoevents@npm:8.0.0" + checksum: 46806fb1bca823de1dac0ec38352c9ebd25dd9f2186eada6357f2941983203c1c77fc383e460852a139b4ced569af2a2777f82cb574f705fd6f8402497f4bfc2 + languageName: node + linkType: hard + "nanoid@npm:3.3.3": version: 3.3.3 resolution: "nanoid@npm:3.3.3" From 9c2d2e5fdbb4aea21dd9480e63051e640881b8e6 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Tue, 11 Jul 2023 10:34:38 +0100 Subject: [PATCH 07/35] experiments(dui3): quick fix for skp bridge init logic/order --- packages/dui3/lib/bridge/sketchup.ts | 17 +++++++++++------ packages/dui3/plugins/00.cefPlugin.ts | 23 ----------------------- 2 files changed, 11 insertions(+), 29 deletions(-) delete mode 100644 packages/dui3/plugins/00.cefPlugin.ts diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index 7342a127b..be8683b2b 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -8,7 +8,9 @@ declare let sketchup: { /** * This class operates in different way than the others, because calls into Sketchup are one way only. * E.g., we cannot return values from internal calls to it (e.g., const test = sketchup.rubyCall() does not work ). - * Values are passed back + * This class basically makes the sketchup bindings work in the same way as cef/webview by returning a promise + * on each method call. That promise is either resolved once sketchup sends back (via receiveResponse) a corresponding + * reply, or it's rejected after a given TIMEOUT_MS (currently 2s). */ export class SketchupBridge { private requests = {} as Record< @@ -27,10 +29,6 @@ export class SketchupBridge { constructor(bindingsName: string) { this.bindingsName = bindingsName || 'default_bindings' - // Initialization continues in the receiveCommandsAndInitializeBridge function, - // where we expect sketchup to return to us the command names. - sketchup.exec({ name: 'get_commands' }) - this.isInitalized = new Promise((resolve, reject) => { this.resolveIsInitializedPromise = resolve setTimeout( @@ -41,6 +39,12 @@ export class SketchupBridge { this.TIMEOUT_MS ) }) + // NOTE: we need to hoist the bindings in global scope here, before + ;(globalThis as Record).bindings = this + + // Initialization continues in the receiveCommandsAndInitializeBridge function, + // where we expect sketchup to return to us the command names. + sketchup.exec({ name: 'get_commands' }) } /** @@ -49,7 +53,8 @@ export class SketchupBridge { * And do JSON.parse() here to get them out properly. * @param commandNames */ - private receiveCommandsAndInitializeBridge(commandNames: string[]) { + private receiveCommandsAndInitializeBridge(commandNamesString: string) { + const commandNames = JSON.parse(commandNamesString) as string[] const hoistTarget = this as unknown as Record for (const commandName of commandNames) { hoistTarget[commandName] = (...args: unknown[]) => diff --git a/packages/dui3/plugins/00.cefPlugin.ts b/packages/dui3/plugins/00.cefPlugin.ts deleted file mode 100644 index 2f852fa86..000000000 --- a/packages/dui3/plugins/00.cefPlugin.ts +++ /dev/null @@ -1,23 +0,0 @@ -// import { ICefSharp, WebUiBindingType, MockedBindings } from '~/types' - -// declare let CefSharp: ICefSharp -// declare let WebUIBinding: WebUiBindingType - -export default defineNuxtPlugin(async () => { - // let bindings: WebUiBindingType - // try { - // if (!CefSharp) throw new Error('No global CefSharp object found.') - // await CefSharp.BindObjectAsync('WebUIBinding') - // console.info('Bound WebUIBinding object for CefSharp.') - // bindings = WebUIBinding - // } catch (e) { - // console.error('Failed to bind CefSharp, will use mocked bindings.') - // console.error(e) - // bindings = MockedBindings - // } - // return { - // provide: { - // bindings - // } - // } -}) From 1958446827e2e51622d598b8608f2cb11eda0713 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Tue, 11 Jul 2023 12:45:16 +0100 Subject: [PATCH 08/35] experiments(dui3): cleanup + comments --- packages/dui3/lib/bridge/sketchup.ts | 13 +++++++++++-- packages/dui3/plugins/00.bindings.ts | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index be8683b2b..1aa5259a3 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -39,11 +39,14 @@ export class SketchupBridge { this.TIMEOUT_MS ) }) - // NOTE: we need to hoist the bindings in global scope here, before + + // NOTE: we need to hoist the bindings in global scope BEFORE we call sketchup exec get comands below. ;(globalThis as Record).bindings = this // Initialization continues in the receiveCommandsAndInitializeBridge function, // where we expect sketchup to return to us the command names. + // NOTE: as we want to have multiple sketchup bindings in the future, we will + // most likely change this method to specify which view/plugin/bindings we want. sketchup.exec({ name: 'get_commands' }) } @@ -72,7 +75,13 @@ export class SketchupBridge { private async runMethod(methodName: string, args: unknown[]): Promise { const requestId = uniqueId(this.bindingsName) - sketchup.exec({ name: methodName, requestId, args }) + // TODO: more on the ruby end, but for now Oguzhan seems happy with this. + // Changes might be needed in the future. + sketchup.exec({ + name: methodName, + // eslint-disable-next-line camelcase + data: { request_id: requestId, arguments: args } + }) return new Promise((resolve, reject) => { this.requests[requestId] = { diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 1eea200be..199344e4e 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -47,6 +47,8 @@ export default defineNuxtPlugin(async () => { console.warn(e) } + // The sketchup ruby side is in flux. We know though that + // this part will work! Nevertheless, it will currently throw if loaded in sketchup. try { if (!sketchup) throw new Error('No global sketchup object found.') console.info('Found Sketchup. Hi SketchUp! We have yet... a lot of work to do :) ') From 2cdc5b7d92d32b978573ef9d99c5cf4ad9b3b3dc Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Tue, 11 Jul 2023 19:16:46 +0100 Subject: [PATCH 09/35] experiments(dui3): adds an event emitter base class --- packages/dui3/lib/bridge/base.ts | 24 ++++++++++++++++++++++++ packages/dui3/lib/bridge/cefSharp.ts | 20 +++----------------- packages/dui3/lib/bridge/sketchup.ts | 11 +++++++---- packages/dui3/lib/bridge/webview.ts | 18 +++--------------- 4 files changed, 37 insertions(+), 36 deletions(-) create mode 100644 packages/dui3/lib/bridge/base.ts diff --git a/packages/dui3/lib/bridge/base.ts b/packages/dui3/lib/bridge/base.ts new file mode 100644 index 000000000..3cc01e9fe --- /dev/null +++ b/packages/dui3/lib/bridge/base.ts @@ -0,0 +1,24 @@ +import { createNanoEvents, Emitter } from 'nanoevents' + +/** + * A simple (typed) event emitter base class that host applications can use to send messages (and data) to the web ui, + * e.g. via `browser.executeScriptAsync("myBindings.on('eventName', serializedData)")`. + + */ +export class BaseBridge { + private emitter: Emitter + + constructor() { + this.emitter = createNanoEvents() + } + + // NOTE: these do not need to be typed extra in here, as they will be properly typed on the specific binding's interface. + on(event: string | number, callback: (...args: unknown[]) => void) { + return this.emitter.on(event, callback) + } + + emit(eventName: string, payload: string) { + const parsedPayload = JSON.parse(payload) as unknown + this.emitter.emit(eventName, parsedPayload) + } +} diff --git a/packages/dui3/lib/bridge/cefSharp.ts b/packages/dui3/lib/bridge/cefSharp.ts index 2d480de24..14c04a201 100644 --- a/packages/dui3/lib/bridge/cefSharp.ts +++ b/packages/dui3/lib/bridge/cefSharp.ts @@ -1,28 +1,14 @@ import { IWebUiBinding } from '~/types' -import { createNanoEvents, Emitter } from 'nanoevents' -import { HostAppEvents } from '~/types' - -export class CefSharpBridge { - private emitter: Emitter +import { BaseBridge } from '~/lib/bridge/base' +export class CefSharpBridge extends BaseBridge { constructor(bindingObject: IWebUiBinding) { + super() const hoistTarget = this as unknown as Record const hoistSource = bindingObject as unknown as Record for (const key in bindingObject) { hoistTarget[key] = hoistSource[key] } - - this.emitter = createNanoEvents() - this.emitter.emit('start', 'polo pasta') - } - - on(event: E, callback: HostAppEvents[E]) { - return this.emitter.on(event, callback) - } - - emit(eventName: string, payload: string) { - const parsedPayload = JSON.parse(payload) as unknown - this.emitter.emit(eventName, parsedPayload) } } diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index 1aa5259a3..471536c0f 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -1,5 +1,5 @@ -// TODO import { uniqueId } from 'lodash-es' +import { BaseBridge } from './base' declare let sketchup: { exec: (data: Record) => void @@ -11,8 +11,9 @@ declare let sketchup: { * This class basically makes the sketchup bindings work in the same way as cef/webview by returning a promise * on each method call. That promise is either resolved once sketchup sends back (via receiveResponse) a corresponding * reply, or it's rejected after a given TIMEOUT_MS (currently 2s). + * TODO: implement the event dispatcher side as well. */ -export class SketchupBridge { +export class SketchupBridge extends BaseBridge { private requests = {} as Record< string, { @@ -27,6 +28,7 @@ export class SketchupBridge { private resolveIsInitializedPromise!: () => unknown constructor(bindingsName: string) { + super() this.bindingsName = bindingsName || 'default_bindings' this.isInitalized = new Promise((resolve, reject) => { @@ -104,8 +106,9 @@ export class SketchupBridge { ) const request = this.requests[requestId] try { - const parsedData = JSON.parse(data) as Record // TODO: check if data is undefined - request.resolve(parsedData) + // NOTE/TODO: does not need parsing + // const parsedData = JSON.parse(data) as Record // TODO: check if data is undefined + request.resolve(data) } catch (e) { request.reject(e as Error) } finally { diff --git a/packages/dui3/lib/bridge/webview.ts b/packages/dui3/lib/bridge/webview.ts index 0817c6edf..7adfc86e3 100644 --- a/packages/dui3/lib/bridge/webview.ts +++ b/packages/dui3/lib/bridge/webview.ts @@ -1,6 +1,5 @@ // github.com/johot/WebView2-better-bridge/blob/master/web-ui/src/betterBridge.ts -import { createNanoEvents, Emitter } from 'nanoevents' -import { HostAppEvents } from '~/types' +import { BaseBridge } from '~/lib/bridge/base' type IWebView2 = { webview: { @@ -17,11 +16,11 @@ type IRawBridge = { declare let chrome: IWebView2 -export class WebView2Bridge { +export class WebView2Bridge extends BaseBridge { private webViewBridge: IRawBridge - private emitter: Emitter constructor(bridgeName: string) { + super() this.webViewBridge = chrome.webview.hostObjects[bridgeName] // NOTE: GetMethods is a call to the .NET side. @@ -36,8 +35,6 @@ export class WebView2Bridge { hoistTarget[lowercasedMethodName] = (...args: unknown[]) => this.runMethod(methodName, args) } - - this.emitter = createNanoEvents() } private async runMethod(methodName: string, args: unknown[]): Promise { @@ -50,15 +47,6 @@ export class WebView2Bridge { return JSON.parse(result) as unknown } - - on(event: E, callback: HostAppEvents[E]) { - return this.emitter.on(event, callback) - } - - emit(eventName: string, payload: string) { - const parsedPayload = JSON.parse(payload) as unknown - this.emitter.emit(eventName, parsedPayload) - } } const lowercaseMethodName = (name: string) => From ca91ff896080b747cc81da45f170a0c744b16e2a Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Tue, 11 Jul 2023 19:17:22 +0100 Subject: [PATCH 10/35] experiments(dui3): adds an event emitter base class --- packages/dui3/lib/bridge/base.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dui3/lib/bridge/base.ts b/packages/dui3/lib/bridge/base.ts index 3cc01e9fe..f8d904fbf 100644 --- a/packages/dui3/lib/bridge/base.ts +++ b/packages/dui3/lib/bridge/base.ts @@ -17,6 +17,7 @@ export class BaseBridge { return this.emitter.on(event, callback) } + // NOTE: this could be private - as it should be only used by the host application. emit(eventName: string, payload: string) { const parsedPayload = JSON.parse(payload) as unknown this.emitter.emit(eventName, parsedPayload) From eb0f4402fb22a8d96039ff46f2b3e65ea5d8881b Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Wed, 12 Jul 2023 19:39:15 +0100 Subject: [PATCH 11/35] experiments(dui3): WIP - needs minor readjustments with the .net patterns --- .../lib/bindings/definitions/baseBindings.ts | 76 +++++++++++++++++++ packages/dui3/lib/bridge/base.ts | 1 - packages/dui3/lib/bridge/webview.ts | 4 +- packages/dui3/pages/index.vue | 2 +- packages/dui3/types/index.ts | 9 +-- 5 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 packages/dui3/lib/bindings/definitions/baseBindings.ts diff --git a/packages/dui3/lib/bindings/definitions/baseBindings.ts b/packages/dui3/lib/bindings/definitions/baseBindings.ts new file mode 100644 index 000000000..83971e527 --- /dev/null +++ b/packages/dui3/lib/bindings/definitions/baseBindings.ts @@ -0,0 +1,76 @@ +export interface IBaseBindings { + sayHi: (name: string) => Promise + getAccounts: () => Promise + getSourceAppName: () => Promise + getSourceAppVersion: () => Promise + getDocumentInfo: () => Promise + + // TODO: + getFileState: () => Promise + updateFileState: (state: FileState) => Promise + + /** + * Subscribe to messages from the host application. + * @param event + * @param callback + */ + on: ( + event: E, + callback: IBaseBindingsHostEvents[E] + ) => void +} + +export interface IBaseBindingsHostEvents { + displayToastNotification: (args: ToastInfo) => void + documentChanged: () => void + selectionChanged: (args: SelectionChangedInfo) => void +} + +export type Account = { + id: string + isDefault: boolean + token: string + serverInfo: { + name: string + url: string + } + userInfo: { + id: string + avatar: string + email: string + name: string + commits: { totalCount: number } + streams: { totalCount: number } + } +} + +export type FileState = { + models: ModelCard[] +} + +export type ModelCard = { + serverUrl: string // we need to select the correct account + modelId: string // we need to assemble the gql query properly + projectId: string + type: 'sender' | 'receiver' + status?: 'idle' | 'inprogress' | 'error' | 'warning' | 'disabled' | 'expired' //??? + // settings: Record??? + // report: Record??? + // progress: Record // ??? send status, receive status +} +export type DocumentInfo = { + location: string + name: string + id: string +} + +export type ToastInfo = { + text: string + details?: string + type: 'info' | 'error' | 'warning' +} + +export type SelectionChangedInfo = { + objectIds: string[] + humanReadableSummary?: string +} diff --git a/packages/dui3/lib/bridge/base.ts b/packages/dui3/lib/bridge/base.ts index f8d904fbf..ae0c6ddf3 100644 --- a/packages/dui3/lib/bridge/base.ts +++ b/packages/dui3/lib/bridge/base.ts @@ -3,7 +3,6 @@ import { createNanoEvents, Emitter } from 'nanoevents' /** * A simple (typed) event emitter base class that host applications can use to send messages (and data) to the web ui, * e.g. via `browser.executeScriptAsync("myBindings.on('eventName', serializedData)")`. - */ export class BaseBridge { private emitter: Emitter diff --git a/packages/dui3/lib/bridge/webview.ts b/packages/dui3/lib/bridge/webview.ts index 7adfc86e3..7878a5b0b 100644 --- a/packages/dui3/lib/bridge/webview.ts +++ b/packages/dui3/lib/bridge/webview.ts @@ -10,7 +10,7 @@ type IWebView2 = { } type IRawBridge = { - GetMethods: () => string[] + GetBindingsMethodNames: () => string[] RunMethod: (methodName: string, args: string) => Promise } @@ -25,7 +25,7 @@ export class WebView2Bridge extends BaseBridge { // NOTE: GetMethods is a call to the .NET side. const availableMethodNames = - chrome.webview.hostObjects.sync[bridgeName].GetMethods() + chrome.webview.hostObjects.sync[bridgeName].GetBindingsMethodNames() // NOTE: hoisting original calls as lowerCasedMethodNames, but using the UpperCasedName for the .NET call // This allows us to follow js convetions and keep .NET ones too (eg. bindings.sayHi('') => public string SayHi(string name) {} diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index 09218cef4..f7f574c0c 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -38,7 +38,7 @@ import { graphql } from '~/lib/common/generated/gql' import { ServerInfoTestQuery } from '~/lib/common/generated/gql/graphql' const { $bindings } = useNuxtApp() -const appName = await $bindings.getSourceAppName() +const appName = await $bindings.getSourceApplicationName() const { accounts, refreshAccounts, defaultAccount } = await useAccountsSetup() const versionQuery = graphql(` diff --git a/packages/dui3/types/index.ts b/packages/dui3/types/index.ts index a7ecd09ab..de7a9a41c 100644 --- a/packages/dui3/types/index.ts +++ b/packages/dui3/types/index.ts @@ -44,19 +44,14 @@ type TestData = { // .NET/Host App -> JS export interface HostAppEvents { test: (data: TestData) => void - documentChanged: (data: { x: number; y: boolean }) => void - selectionChanged: () => void - documentClosed: () => void - updateModelCardState: () => void - displayToastNotification: () => void // bla bla bla } // JS -> asks for something form host app export interface IWebUiBinding { - sayHi: (name: string) => Promise openDevTools: () => Promise getAccounts: () => Promise - getSourceAppName: () => Promise + getSourceApplicationName: () => Promise + getSourceApplicationVersion: () => Promise // getFileState: () => Promise // addModelCard(string modelId, string projectId), removeModelCard(...) // etc. etc. From 2467b4749f0ebd99be1fc9393a1fef5761a9fb91 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Thu, 13 Jul 2023 15:04:18 +0100 Subject: [PATCH 12/35] experiments(dui3): introducing generic bridge (cef/webview2) + some cleanup --- .../dui3/lib/accounts/composables/setup.ts | 6 +- .../lib/bindings/definitions/baseBindings.ts | 19 ++- packages/dui3/lib/bridge/cefSharp.ts | 24 ++-- packages/dui3/lib/bridge/generic.ts | 45 ++++++ packages/dui3/pages/index.vue | 4 +- packages/dui3/plugins/00.bindings.ts | 135 +++++++++--------- packages/dui3/plugins/00.bindingsV2.ts | 51 +++++++ packages/dui3/types/index.ts | 93 ------------ 8 files changed, 189 insertions(+), 188 deletions(-) create mode 100644 packages/dui3/lib/bridge/generic.ts create mode 100644 packages/dui3/plugins/00.bindingsV2.ts delete mode 100644 packages/dui3/types/index.ts diff --git a/packages/dui3/lib/accounts/composables/setup.ts b/packages/dui3/lib/accounts/composables/setup.ts index 8e4a47e97..31339278f 100644 --- a/packages/dui3/lib/accounts/composables/setup.ts +++ b/packages/dui3/lib/accounts/composables/setup.ts @@ -1,8 +1,8 @@ import { ApolloClient } from '@apollo/client/core' import { ApolloClients } from '@vue/apollo-composable' import { ShallowRef, ComputedRef } from 'vue' +import { Account } from '~/lib/bindings/definitions/baseBindings' import { resolveClientConfig } from '~/lib/core/configs/apollo' -import { Account } from '~/types' export type DUIAccount = { /** account info coming from the host app */ @@ -21,7 +21,7 @@ const AccountsInjectionKey = 'DUI_ACCOUNTS_STATE' export async function useAccountsSetup(): Promise { const app = useNuxtApp() - const $bindings = app.$bindings + const $baseBinding = app.$baseBinding // Using a shallow ref as we don't need inner values reactive (could be a needlessly big return from host app) const accounts = shallowRef([] as DUIAccount[]) @@ -30,7 +30,7 @@ export async function useAccountsSetup(): Promise { // Matches local accounts coming from the host app to app state. const refreshAccounts = async () => { - const accs = await $bindings.getAccounts() + const accs = await $baseBinding.getAccounts() const newAccs = [] as DUIAccount[] for (const acc of accs) { const existing = accounts.value.find((a) => a.accountInfo.id === acc.id) diff --git a/packages/dui3/lib/bindings/definitions/baseBindings.ts b/packages/dui3/lib/bindings/definitions/baseBindings.ts index 83971e527..2b5c0407d 100644 --- a/packages/dui3/lib/bindings/definitions/baseBindings.ts +++ b/packages/dui3/lib/bindings/definitions/baseBindings.ts @@ -1,8 +1,13 @@ -export interface IBaseBindings { - sayHi: (name: string) => Promise +// Needs to be agreed between Frontend and Rhino +export interface IRhinoRandomBinding { + makeGreeting: (name: string) => Promise +} + +// Needs to be agreed between Frontend and Core +export interface IBaseBinding { getAccounts: () => Promise - getSourceAppName: () => Promise - getSourceAppVersion: () => Promise + getSourceApplicationName: () => Promise + getSourceApplicationVersion: () => Promise getDocumentInfo: () => Promise // TODO: @@ -14,13 +19,13 @@ export interface IBaseBindings { * @param event * @param callback */ - on: ( + on: ( event: E, - callback: IBaseBindingsHostEvents[E] + callback: IBaseBindingHostEvents[E] ) => void } -export interface IBaseBindingsHostEvents { +export interface IBaseBindingHostEvents { displayToastNotification: (args: ToastInfo) => void documentChanged: () => void selectionChanged: (args: SelectionChangedInfo) => void diff --git a/packages/dui3/lib/bridge/cefSharp.ts b/packages/dui3/lib/bridge/cefSharp.ts index 14c04a201..08ed31e0e 100644 --- a/packages/dui3/lib/bridge/cefSharp.ts +++ b/packages/dui3/lib/bridge/cefSharp.ts @@ -1,14 +1,14 @@ -import { IWebUiBinding } from '~/types' -import { BaseBridge } from '~/lib/bridge/base' +// import { IWebUiBinding } from '~/types' +// import { BaseBridge } from '~/lib/bridge/base' -export class CefSharpBridge extends BaseBridge { - constructor(bindingObject: IWebUiBinding) { - super() - const hoistTarget = this as unknown as Record - const hoistSource = bindingObject as unknown as Record +// export class CefSharpBridge extends BaseBridge { +// constructor(bindingObject: IWebUiBinding) { +// super() +// const hoistTarget = this as unknown as Record +// const hoistSource = bindingObject as unknown as Record - for (const key in bindingObject) { - hoistTarget[key] = hoistSource[key] - } - } -} +// for (const key in bindingObject) { +// hoistTarget[key] = hoistSource[key] +// } +// } +// } diff --git a/packages/dui3/lib/bridge/generic.ts b/packages/dui3/lib/bridge/generic.ts new file mode 100644 index 000000000..50170669a --- /dev/null +++ b/packages/dui3/lib/bridge/generic.ts @@ -0,0 +1,45 @@ +// github.com/johot/WebView2-better-bridge/blob/master/web-ui/src/betterBridge.ts +import { BaseBridge } from '~/lib/bridge/base' + +export type IRawBridge = { + GetBindingsMethodNames: () => Promise + RunMethod: (methodName: string, args: string) => Promise + ShowDevTools: () => Promise +} + +export class GenericBridge extends BaseBridge { + private bridge: IRawBridge + + constructor(object: IRawBridge) { + super() + this.bridge = object + } + + public async create() { + // NOTE: GetMethods is a call to the .NET side. + const availableMethodNames = await this.bridge.GetBindingsMethodNames() + + // NOTE: hoisting original calls as lowerCasedMethodNames, but using the UpperCasedName for the .NET call + // This allows us to follow js convetions and keep .NET ones too (eg. bindings.sayHi('') => public string SayHi(string name) {} + for (const methodName of availableMethodNames) { + const lowercasedMethodName = lowercaseMethodName(methodName) + const hoistTarget = this as unknown as Record + hoistTarget[lowercasedMethodName] = (...args: unknown[]) => + this.runMethod(methodName, args) + } + } + + private async runMethod(methodName: string, args: unknown[]): Promise { + const preserializedArgs = args.map((a) => JSON.stringify(a)) + // NOTE: RunMethod is a call to the .NET side. + const result = await this.bridge.RunMethod( + methodName, + JSON.stringify(preserializedArgs) + ) + + return JSON.parse(result) as unknown + } +} + +const lowercaseMethodName = (name: string) => + name.charAt(0).toLowerCase() + name.slice(1) diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index f7f574c0c..6f03802db 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -37,8 +37,8 @@ import { useAccountsSetup } from '~/lib/accounts/composables/setup' import { graphql } from '~/lib/common/generated/gql' import { ServerInfoTestQuery } from '~/lib/common/generated/gql/graphql' -const { $bindings } = useNuxtApp() -const appName = await $bindings.getSourceApplicationName() +const { $baseBinding } = useNuxtApp() +const appName = await $baseBinding.getSourceApplicationName() const { accounts, refreshAccounts, defaultAccount } = await useAccountsSetup() const versionQuery = graphql(` diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 199344e4e..a21617061 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -1,82 +1,75 @@ -import { IWebUiBinding, MockedBindings } from '~/types' -import { WebView2Bridge } from '~/lib/bridge/webview' -import { CefSharpBridge } from '~/lib/bridge/cefSharp' -import { SketchupBridge } from '~/lib/bridge/sketchup' +// import { IWebUiBinding, MockedBindings } from '~/types' +// import { WebView2Bridge } from '~/lib/bridge/webview' +// import { CefSharpBridge } from '~/lib/bridge/cefSharp' +// import { SketchupBridge } from '~/lib/bridge/sketchup' -interface ICefSharp { - BindObjectAsync: (arg: string) => Promise -} +// interface ICefSharp { +// BindObjectAsync: (arg: string) => Promise +// } -interface IWebView2 { - webview: unknown -} +// interface IWebView2 { +// webview: unknown +// } -declare let CefSharp: ICefSharp -declare let chrome: IWebView2 -declare let sketchup: Record +// declare let CefSharp: ICefSharp +// declare let chrome: IWebView2 +// declare let sketchup: Record -declare let WebUIBinding: IWebUiBinding +// declare let WebUIBinding: IWebUiBinding // Tries to find the correct host application binding. The sequence is: // - CEFSharp (.NET) // - WebView2 (.NET) // - Sketchup (Ruby) - NOT IMPLEMENTED export default defineNuxtPlugin(async () => { - let bindings: IWebUiBinding | undefined = undefined - - try { - if (!CefSharp) throw new Error('No global CefSharp object found.') - await CefSharp.BindObjectAsync('WebUIBinding') - console.info('Bound WebUIBinding object for CefSharp.') - bindings = new CefSharpBridge(WebUIBinding) as unknown as IWebUiBinding - } catch (e) { - console.warn( - 'Failed to bind CefSharp. This can be totally normal if the host is different.' - ) - console.warn(e) - } - - try { - if (!chrome.webview) throw new Error('No global Webview2 object found.') - bindings = new WebView2Bridge('WebUIBinding') as unknown as IWebUiBinding - console.info('Bound WebUIBinding object for Webview2.') - } catch (e) { - console.warn( - 'Failed to bind Webview2. This can be totally normal if the host is different.' - ) - console.warn(e) - } - - // The sketchup ruby side is in flux. We know though that - // this part will work! Nevertheless, it will currently throw if loaded in sketchup. - try { - if (!sketchup) throw new Error('No global sketchup object found.') - console.info('Found Sketchup. Hi SketchUp! We have yet... a lot of work to do :) ') - - const skpBindings = new SketchupBridge('default_bindings') - // Note, because of the way Sketchup bindings work, we need to wait here - // for them to be fully initialized. - await skpBindings.isInitalized - bindings = skpBindings as unknown as IWebUiBinding - } catch (e) { - console.warn( - 'Failed to bind sketchup. This can be totally normal if the host is different.' - ) - console.warn(e) - } - - if (!bindings) { - console.warn('No bindings found - falling back to mocked bindings.') - bindings = MockedBindings - } - - // We need the bindings object in global scope to allow - // host applications to send messages back to it. - ;(globalThis as Record).bindings = bindings - - return { - provide: { - bindings - } - } + // let bindings: IWebUiBinding | undefined = undefined + // try { + // if (!CefSharp) throw new Error('No global CefSharp object found.') + // await CefSharp.BindObjectAsync('WebUIBinding') + // console.info('Bound WebUIBinding object for CefSharp.') + // bindings = new CefSharpBridge(WebUIBinding) as unknown as IWebUiBinding + // } catch (e) { + // console.warn( + // 'Failed to bind CefSharp. This can be totally normal if the host is different.' + // ) + // console.warn(e) + // } + // try { + // if (!chrome.webview) throw new Error('No global Webview2 object found.') + // bindings = new WebView2Bridge('WebUIBinding') as unknown as IWebUiBinding + // console.info('Bound WebUIBinding object for Webview2.') + // } catch (e) { + // console.warn( + // 'Failed to bind Webview2. This can be totally normal if the host is different.' + // ) + // console.warn(e) + // } + // // The sketchup ruby side is in flux. We know though that + // // this part will work! Nevertheless, it will currently throw if loaded in sketchup. + // try { + // if (!sketchup) throw new Error('No global sketchup object found.') + // console.info('Found Sketchup. Hi SketchUp! We have yet... a lot of work to do :) ') + // const skpBindings = new SketchupBridge('default_bindings') + // // Note, because of the way Sketchup bindings work, we need to wait here + // // for them to be fully initialized. + // await skpBindings.isInitalized + // bindings = skpBindings as unknown as IWebUiBinding + // } catch (e) { + // console.warn( + // 'Failed to bind sketchup. This can be totally normal if the host is different.' + // ) + // console.warn(e) + // } + // if (!bindings) { + // console.warn('No bindings found - falling back to mocked bindings.') + // bindings = MockedBindings + // } + // // We need the bindings object in global scope to allow + // // host applications to send messages back to it. + // ;(globalThis as Record).bindings = bindings + // return { + // provide: { + // bindings + // } + // } }) diff --git a/packages/dui3/plugins/00.bindingsV2.ts b/packages/dui3/plugins/00.bindingsV2.ts new file mode 100644 index 000000000..c768cc9ae --- /dev/null +++ b/packages/dui3/plugins/00.bindingsV2.ts @@ -0,0 +1,51 @@ +import { GenericBridge, IRawBridge } from '~/lib/bridge/generic' +import { + IBaseBinding, + IRhinoRandomBinding +} from '~/lib/bindings/definitions/baseBindings' + +// Makes TS happy +declare let globalThis: Record & { + CefSharp?: { BindObjectAsync: (name: string) => Promise } + chrome?: { webview: { hostObjects: Record } } + sketchup?: Record +} + +// eslint-disable-next-line @typescript-eslint/require-await +const tryHoistBinding = async (name: string) => { + let bridge: GenericBridge | null = null + + if (globalThis.CefSharp) { + await globalThis.CefSharp.BindObjectAsync(name) + bridge = new GenericBridge(globalThis[name] as unknown as IRawBridge) + await bridge.create() + } + + if (globalThis.chrome && !bridge) { + bridge = new GenericBridge(globalThis.chrome.webview.hostObjects[name]) + await bridge.create() + } + + if (globalThis.sketchup && !bridge) { + // TODO + } + + if (!bridge) console.warn(`Failed to bind ${name} binding.`) + + globalThis[name] = bridge + return bridge as unknown as T +} + +export default defineNuxtPlugin(async () => { + const baseBinding = await tryHoistBinding('baseBinding') + const rhinoRandomBinding = await tryHoistBinding( + 'rhinoRandomBinding' + ) + + return { + provide: { + baseBinding, + rhinoRandomBinding + } + } +}) diff --git a/packages/dui3/types/index.ts b/packages/dui3/types/index.ts deleted file mode 100644 index de7a9a41c..000000000 --- a/packages/dui3/types/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-disable @typescript-eslint/require-await */ -import { createNanoEvents } from 'nanoevents' - -export type Account = { - id: string - isDefault: boolean - token: string - serverInfo: { - name: string - url: string - } - userInfo: { - id: string - avatar: string - email: string - name: string - commits: { totalCount: number } - streams: { totalCount: number } - } -} - -type FileState = { - models: ModelCard[] -} - -type ModelCard = { - serverUrl: string // we need to select the correct account - modelId: string // we need to assemble the gql query properly - projectId: string - type: 'sender' | 'receiver' - lastUpdatedAt: Date - // settings: Record??? - // report: Record??? - // progress: Record??? - status: 'idle' | 'inprogress' | 'error' | 'warning' | 'disabled' -} - -type TestData = { - foo: number - bar: string - baz: boolean -} - -// .NET/Host App -> JS -export interface HostAppEvents { - test: (data: TestData) => void -} - -// JS -> asks for something form host app -export interface IWebUiBinding { - openDevTools: () => Promise - getAccounts: () => Promise - getSourceApplicationName: () => Promise - getSourceApplicationVersion: () => Promise - // getFileState: () => Promise - // addModelCard(string modelId, string projectId), removeModelCard(...) // etc. etc. - - /** - * Subscribe to messages from the host application. - * @param event - * @param callback - */ - on: (event: E, callback: HostAppEvents[E]) => void - /** - * Used by the host application to notify/send data to the web app. Do not use from the frontend. - * @param eventName - * @param args - */ - emit?: (eventName: string, args: Record) => void -} - -const mockedEmitter = createNanoEvents() -export const MockedBindings: IWebUiBinding = { - async sayHi(name: string) { - return `Hi ${name} from (mocked bindings)!` - }, - async openDevTools() { - // eslint-disable-next-line no-alert - window.alert('Mocked bindings cannot do this. Sorry :(') - }, - async getAccounts() { - return [] - }, - async getSourceAppName() { - return 'Mocked App' - }, - async getFileState() { - return { models: [] } - }, - on(event: E, callback: HostAppEvents[E]) { - return mockedEmitter.on(event, callback) - } -} From 203ca507fbc107cfa6c304f760b98b9897b8b286 Mon Sep 17 00:00:00 2001 From: oguzhankoral Date: Fri, 14 Jul 2023 02:52:14 +0300 Subject: [PATCH 13/35] Implement sketchup bridge to tryHoistBindings --- packages/dui3/lib/bridge/sketchup.ts | 29 ++++++++++++++++++++------ packages/dui3/plugins/00.bindingsV2.ts | 10 ++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index 471536c0f..db2e20ccc 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -3,6 +3,7 @@ import { BaseBridge } from './base' declare let sketchup: { exec: (data: Record) => void + getCommands: (viewId: string) => void } /** @@ -24,8 +25,9 @@ export class SketchupBridge extends BaseBridge { > private bindingsName: string private TIMEOUT_MS = 2000 // 2s - public isInitalized: Promise - private resolveIsInitializedPromise!: () => unknown + public isInitalized: Promise + private resolveIsInitializedPromise!: (v: boolean) => unknown + private rejectIsInitializedPromise!: (message: string) => unknown constructor(bindingsName: string) { super() @@ -33,6 +35,7 @@ export class SketchupBridge extends BaseBridge { this.isInitalized = new Promise((resolve, reject) => { this.resolveIsInitializedPromise = resolve + this.rejectIsInitializedPromise = reject setTimeout( () => reject( @@ -46,10 +49,12 @@ export class SketchupBridge extends BaseBridge { ;(globalThis as Record).bindings = this // Initialization continues in the receiveCommandsAndInitializeBridge function, - // where we expect sketchup to return to us the command names. + // where we expect sketchup to return to us the command names for related bindings/views. // NOTE: as we want to have multiple sketchup bindings in the future, we will // most likely change this method to specify which view/plugin/bindings we want. - sketchup.exec({ name: 'get_commands' }) + // eslint-disable-next-line camelcase + // sketchup.exec({ name: 'getCommands', view_id: this.bindingsName }) + sketchup.getCommands(this.bindingsName) } /** @@ -66,7 +71,16 @@ export class SketchupBridge extends BaseBridge { this.runMethod(commandName, args) } - this.resolveIsInitializedPromise() + this.resolveIsInitializedPromise(true) + } + + /** + * Will be called by `executeScript('bindings.rejectBindings()')` from sketchup. + * @param message + */ + private rejectBindings(message: string) { + this.resolveIsInitializedPromise(false) + this.rejectIsInitializedPromise(message) } /** @@ -82,7 +96,10 @@ export class SketchupBridge extends BaseBridge { sketchup.exec({ name: methodName, // eslint-disable-next-line camelcase - data: { request_id: requestId, arguments: args } + request_id: requestId, + // eslint-disable-next-line camelcase + view_id: this.bindingsName, + data: { args } }) return new Promise((resolve, reject) => { diff --git a/packages/dui3/plugins/00.bindingsV2.ts b/packages/dui3/plugins/00.bindingsV2.ts index c768cc9ae..187a59c1e 100644 --- a/packages/dui3/plugins/00.bindingsV2.ts +++ b/packages/dui3/plugins/00.bindingsV2.ts @@ -3,6 +3,7 @@ import { IBaseBinding, IRhinoRandomBinding } from '~/lib/bindings/definitions/baseBindings' +import { SketchupBridge } from '~/lib/bridge/sketchup' // Makes TS happy declare let globalThis: Record & { @@ -13,7 +14,7 @@ declare let globalThis: Record & { // eslint-disable-next-line @typescript-eslint/require-await const tryHoistBinding = async (name: string) => { - let bridge: GenericBridge | null = null + let bridge: GenericBridge | SketchupBridge | null = null if (globalThis.CefSharp) { await globalThis.CefSharp.BindObjectAsync(name) @@ -21,13 +22,16 @@ const tryHoistBinding = async (name: string) => { await bridge.create() } - if (globalThis.chrome && !bridge) { + if (globalThis.chrome && globalThis.chrome.webview && !bridge) { bridge = new GenericBridge(globalThis.chrome.webview.hostObjects[name]) await bridge.create() } if (globalThis.sketchup && !bridge) { - // TODO + bridge = new SketchupBridge(name) + const res = await bridge.isInitalized + // If bridge doesn't initialized succesffuly, then do not create binding + if (!res) bridge = null } if (!bridge) console.warn(`Failed to bind ${name} binding.`) From 047a7644a21ed0b6bbabfb801d5edec335a36d9a Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Fri, 14 Jul 2023 08:13:22 +0100 Subject: [PATCH 14/35] experiments(dui3): cleanup --- .../lib/bindings/definitions/baseBindings.ts | 1 - packages/dui3/lib/bridge/base.ts | 2 +- packages/dui3/lib/bridge/cefSharp.ts | 14 -- packages/dui3/lib/bridge/definitions/index.ts | 8 ++ packages/dui3/lib/bridge/generic.ts | 12 +- packages/dui3/lib/bridge/webview.ts | 53 ------- packages/dui3/lib/document-info/index.ts | 25 ++++ packages/dui3/pages/index.vue | 14 +- packages/dui3/plugins/00.bindings.ts | 129 ++++++++---------- packages/dui3/plugins/00.bindingsV2.ts | 51 ------- 10 files changed, 105 insertions(+), 204 deletions(-) delete mode 100644 packages/dui3/lib/bridge/cefSharp.ts create mode 100644 packages/dui3/lib/bridge/definitions/index.ts delete mode 100644 packages/dui3/lib/bridge/webview.ts create mode 100644 packages/dui3/lib/document-info/index.ts delete mode 100644 packages/dui3/plugins/00.bindingsV2.ts diff --git a/packages/dui3/lib/bindings/definitions/baseBindings.ts b/packages/dui3/lib/bindings/definitions/baseBindings.ts index 2b5c0407d..d6170caa0 100644 --- a/packages/dui3/lib/bindings/definitions/baseBindings.ts +++ b/packages/dui3/lib/bindings/definitions/baseBindings.ts @@ -28,7 +28,6 @@ export interface IBaseBinding { export interface IBaseBindingHostEvents { displayToastNotification: (args: ToastInfo) => void documentChanged: () => void - selectionChanged: (args: SelectionChangedInfo) => void } export type Account = { diff --git a/packages/dui3/lib/bridge/base.ts b/packages/dui3/lib/bridge/base.ts index ae0c6ddf3..ade5aa42c 100644 --- a/packages/dui3/lib/bridge/base.ts +++ b/packages/dui3/lib/bridge/base.ts @@ -18,7 +18,7 @@ export class BaseBridge { // NOTE: this could be private - as it should be only used by the host application. emit(eventName: string, payload: string) { - const parsedPayload = JSON.parse(payload) as unknown + const parsedPayload = payload ? (JSON.parse(payload) as unknown) : null this.emitter.emit(eventName, parsedPayload) } } diff --git a/packages/dui3/lib/bridge/cefSharp.ts b/packages/dui3/lib/bridge/cefSharp.ts deleted file mode 100644 index 08ed31e0e..000000000 --- a/packages/dui3/lib/bridge/cefSharp.ts +++ /dev/null @@ -1,14 +0,0 @@ -// import { IWebUiBinding } from '~/types' -// import { BaseBridge } from '~/lib/bridge/base' - -// export class CefSharpBridge extends BaseBridge { -// constructor(bindingObject: IWebUiBinding) { -// super() -// const hoistTarget = this as unknown as Record -// const hoistSource = bindingObject as unknown as Record - -// for (const key in bindingObject) { -// hoistTarget[key] = hoistSource[key] -// } -// } -// } diff --git a/packages/dui3/lib/bridge/definitions/index.ts b/packages/dui3/lib/bridge/definitions/index.ts new file mode 100644 index 000000000..3eb9dd242 --- /dev/null +++ b/packages/dui3/lib/bridge/definitions/index.ts @@ -0,0 +1,8 @@ +/** + * Defines the expected contract of the host application bound object. + */ +export type IRawBridge = { + GetBindingsMethodNames: () => Promise + RunMethod: (methodName: string, args: string) => Promise + ShowDevTools: () => Promise +} diff --git a/packages/dui3/lib/bridge/generic.ts b/packages/dui3/lib/bridge/generic.ts index 50170669a..a3afa7b1a 100644 --- a/packages/dui3/lib/bridge/generic.ts +++ b/packages/dui3/lib/bridge/generic.ts @@ -1,12 +1,8 @@ -// github.com/johot/WebView2-better-bridge/blob/master/web-ui/src/betterBridge.ts import { BaseBridge } from '~/lib/bridge/base' - -export type IRawBridge = { - GetBindingsMethodNames: () => Promise - RunMethod: (methodName: string, args: string) => Promise - ShowDevTools: () => Promise -} - +import { IRawBridge } from '~/lib/bridge/definitions' +/** + * A generic bridge class for Webivew2 or CefSharp. + */ export class GenericBridge extends BaseBridge { private bridge: IRawBridge diff --git a/packages/dui3/lib/bridge/webview.ts b/packages/dui3/lib/bridge/webview.ts deleted file mode 100644 index 7878a5b0b..000000000 --- a/packages/dui3/lib/bridge/webview.ts +++ /dev/null @@ -1,53 +0,0 @@ -// github.com/johot/WebView2-better-bridge/blob/master/web-ui/src/betterBridge.ts -import { BaseBridge } from '~/lib/bridge/base' - -type IWebView2 = { - webview: { - hostObjects: Record & { - sync: Record - } - } -} - -type IRawBridge = { - GetBindingsMethodNames: () => string[] - RunMethod: (methodName: string, args: string) => Promise -} - -declare let chrome: IWebView2 - -export class WebView2Bridge extends BaseBridge { - private webViewBridge: IRawBridge - - constructor(bridgeName: string) { - super() - this.webViewBridge = chrome.webview.hostObjects[bridgeName] - - // NOTE: GetMethods is a call to the .NET side. - const availableMethodNames = - chrome.webview.hostObjects.sync[bridgeName].GetBindingsMethodNames() - - // NOTE: hoisting original calls as lowerCasedMethodNames, but using the UpperCasedName for the .NET call - // This allows us to follow js convetions and keep .NET ones too (eg. bindings.sayHi('') => public string SayHi(string name) {} - for (const methodName of availableMethodNames) { - const lowercasedMethodName = lowercaseMethodName(methodName) - const hoistTarget = this as unknown as Record - hoistTarget[lowercasedMethodName] = (...args: unknown[]) => - this.runMethod(methodName, args) - } - } - - private async runMethod(methodName: string, args: unknown[]): Promise { - const preserializedArgs = args.map((a) => JSON.stringify(a)) - // NOTE: RunMethod is a call to the .NET side. - const result = await this.webViewBridge.RunMethod( - methodName, - JSON.stringify(preserializedArgs) - ) - - return JSON.parse(result) as unknown - } -} - -const lowercaseMethodName = (name: string) => - name.charAt(0).toLowerCase() + name.slice(1) diff --git a/packages/dui3/lib/document-info/index.ts b/packages/dui3/lib/document-info/index.ts new file mode 100644 index 000000000..ff7f00118 --- /dev/null +++ b/packages/dui3/lib/document-info/index.ts @@ -0,0 +1,25 @@ +import { DocumentInfo } from '~/lib/bindings/definitions/baseBindings' +import { ref } from 'vue' + +const DocumentInfoInjectionKey = 'DUI_ACCOUNTS_STATE' + +export async function useDocumentInfoSetup() { + const app = useNuxtApp() + const documentInfo = ref() + + app.$baseBinding.on('documentChanged', () => { + setTimeout(async () => { + const docInfo = await app.$baseBinding.getDocumentInfo() + documentInfo.value = docInfo + }, 500) // Don't ask + }) + + documentInfo.value = await app.$baseBinding.getDocumentInfo() + provide(DocumentInfoInjectionKey, documentInfo) + return documentInfo +} + +export function useInjectedDocumentInfo() { + const documentInfo = inject>(DocumentInfoInjectionKey) + return documentInfo +} diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index 6f03802db..6242797a3 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -4,7 +4,6 @@
for {{ appName }}
-
@@ -18,13 +17,17 @@ {{ acc.accountInfo.serverInfo.name }}
-
Your default account is {{ defaultAccount?.accountInfo }}
+
+ Your default account is at {{ defaultAccount?.accountInfo.serverInfo.url }} +
{{ clientId }}: {{ res.result.value?.serverInfo.version || res.error }}
+
Doc info:
+
{{ documentInfo }}
Refresh Accounts
@@ -36,11 +39,14 @@ import { UseQueryReturn, useQuery } from '@vue/apollo-composable' import { useAccountsSetup } from '~/lib/accounts/composables/setup' import { graphql } from '~/lib/common/generated/gql' import { ServerInfoTestQuery } from '~/lib/common/generated/gql/graphql' +import { useDocumentInfoSetup } from '~/lib/document-info' const { $baseBinding } = useNuxtApp() const appName = await $baseBinding.getSourceApplicationName() const { accounts, refreshAccounts, defaultAccount } = await useAccountsSetup() +const documentInfo = await useDocumentInfoSetup() + const versionQuery = graphql(` query ServerInfoTest { serverInfo { @@ -49,10 +55,6 @@ const versionQuery = graphql(` } `) -watch(accounts, () => { - console.log('accounts were refreshed, shallow ref does its job') -}) - const clientIds = accounts.value.map((a) => a.accountInfo.id) const queries: Record< diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index a21617061..61eb51f5a 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -1,75 +1,64 @@ -// import { IWebUiBinding, MockedBindings } from '~/types' -// import { WebView2Bridge } from '~/lib/bridge/webview' -// import { CefSharpBridge } from '~/lib/bridge/cefSharp' -// import { SketchupBridge } from '~/lib/bridge/sketchup' +import { GenericBridge } from '~/lib/bridge/generic' +import { IRawBridge } from '~/lib/bridge/definitions' -// interface ICefSharp { -// BindObjectAsync: (arg: string) => Promise -// } +import { + IBaseBinding, + IRhinoRandomBinding +} from '~/lib/bindings/definitions/baseBindings' -// interface IWebView2 { -// webview: unknown -// } +// Makes TS happy +declare let globalThis: Record & { + CefSharp?: { BindObjectAsync: (name: string) => Promise } + chrome?: { webview: { hostObjects: Record } } + sketchup?: Record +} -// declare let CefSharp: ICefSharp -// declare let chrome: IWebView2 -// declare let sketchup: Record - -// declare let WebUIBinding: IWebUiBinding - -// Tries to find the correct host application binding. The sequence is: -// - CEFSharp (.NET) -// - WebView2 (.NET) -// - Sketchup (Ruby) - NOT IMPLEMENTED +/** + * Here we are loading any bindings that we expect to have from all + * connectors. If some are not present, that's okay - we're going to + * strip or customize functionality from the ui itself. + */ export default defineNuxtPlugin(async () => { - // let bindings: IWebUiBinding | undefined = undefined - // try { - // if (!CefSharp) throw new Error('No global CefSharp object found.') - // await CefSharp.BindObjectAsync('WebUIBinding') - // console.info('Bound WebUIBinding object for CefSharp.') - // bindings = new CefSharpBridge(WebUIBinding) as unknown as IWebUiBinding - // } catch (e) { - // console.warn( - // 'Failed to bind CefSharp. This can be totally normal if the host is different.' - // ) - // console.warn(e) - // } - // try { - // if (!chrome.webview) throw new Error('No global Webview2 object found.') - // bindings = new WebView2Bridge('WebUIBinding') as unknown as IWebUiBinding - // console.info('Bound WebUIBinding object for Webview2.') - // } catch (e) { - // console.warn( - // 'Failed to bind Webview2. This can be totally normal if the host is different.' - // ) - // console.warn(e) - // } - // // The sketchup ruby side is in flux. We know though that - // // this part will work! Nevertheless, it will currently throw if loaded in sketchup. - // try { - // if (!sketchup) throw new Error('No global sketchup object found.') - // console.info('Found Sketchup. Hi SketchUp! We have yet... a lot of work to do :) ') - // const skpBindings = new SketchupBridge('default_bindings') - // // Note, because of the way Sketchup bindings work, we need to wait here - // // for them to be fully initialized. - // await skpBindings.isInitalized - // bindings = skpBindings as unknown as IWebUiBinding - // } catch (e) { - // console.warn( - // 'Failed to bind sketchup. This can be totally normal if the host is different.' - // ) - // console.warn(e) - // } - // if (!bindings) { - // console.warn('No bindings found - falling back to mocked bindings.') - // bindings = MockedBindings - // } - // // We need the bindings object in global scope to allow - // // host applications to send messages back to it. - // ;(globalThis as Record).bindings = bindings - // return { - // provide: { - // bindings - // } - // } + const baseBinding = await tryHoistBinding('baseBinding') + + const rhinoRandomBinding = await tryHoistBinding( + 'rhinoRandomBinding' + ) + + return { + provide: { + baseBinding, + rhinoRandomBinding + } + } }) + +/** + * Checks possible browser window targets for a given binding, and, if it finds it, + * creates a bridge for it and registers it in the global scope. + * @param name binding name + * @returns null if the binding was not found, or the binding. + */ +const tryHoistBinding = async (name: string) => { + let bridge: GenericBridge | null = null + + if (globalThis.CefSharp) { + await globalThis.CefSharp.BindObjectAsync(name) + bridge = new GenericBridge(globalThis[name] as unknown as IRawBridge) + await bridge.create() + } + + if (globalThis.chrome && !bridge) { + bridge = new GenericBridge(globalThis.chrome.webview.hostObjects[name]) + await bridge.create() + } + + if (globalThis.sketchup && !bridge) { + // TODO + } + + if (!bridge) console.warn(`Failed to bind ${name} binding.`) + + globalThis[name] = bridge + return bridge as unknown as T +} diff --git a/packages/dui3/plugins/00.bindingsV2.ts b/packages/dui3/plugins/00.bindingsV2.ts deleted file mode 100644 index c768cc9ae..000000000 --- a/packages/dui3/plugins/00.bindingsV2.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { GenericBridge, IRawBridge } from '~/lib/bridge/generic' -import { - IBaseBinding, - IRhinoRandomBinding -} from '~/lib/bindings/definitions/baseBindings' - -// Makes TS happy -declare let globalThis: Record & { - CefSharp?: { BindObjectAsync: (name: string) => Promise } - chrome?: { webview: { hostObjects: Record } } - sketchup?: Record -} - -// eslint-disable-next-line @typescript-eslint/require-await -const tryHoistBinding = async (name: string) => { - let bridge: GenericBridge | null = null - - if (globalThis.CefSharp) { - await globalThis.CefSharp.BindObjectAsync(name) - bridge = new GenericBridge(globalThis[name] as unknown as IRawBridge) - await bridge.create() - } - - if (globalThis.chrome && !bridge) { - bridge = new GenericBridge(globalThis.chrome.webview.hostObjects[name]) - await bridge.create() - } - - if (globalThis.sketchup && !bridge) { - // TODO - } - - if (!bridge) console.warn(`Failed to bind ${name} binding.`) - - globalThis[name] = bridge - return bridge as unknown as T -} - -export default defineNuxtPlugin(async () => { - const baseBinding = await tryHoistBinding('baseBinding') - const rhinoRandomBinding = await tryHoistBinding( - 'rhinoRandomBinding' - ) - - return { - provide: { - baseBinding, - rhinoRandomBinding - } - } -}) From bf84060f5e6d306defa4a00b7b7b4cf586cd40fe Mon Sep 17 00:00:00 2001 From: oguzhankoral Date: Fri, 14 Jul 2023 12:49:26 +0300 Subject: [PATCH 15/35] Create SketchupBridge for tryHoistBinding --- packages/dui3/lib/bridge/base.ts | 2 ++ packages/dui3/lib/bridge/sketchup.ts | 3 +++ packages/dui3/plugins/00.bindings.ts | 14 ++++++++++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/dui3/lib/bridge/base.ts b/packages/dui3/lib/bridge/base.ts index ade5aa42c..8d0e1e67f 100644 --- a/packages/dui3/lib/bridge/base.ts +++ b/packages/dui3/lib/bridge/base.ts @@ -18,6 +18,8 @@ export class BaseBridge { // NOTE: this could be private - as it should be only used by the host application. emit(eventName: string, payload: string) { + console.log(payload) + const parsedPayload = payload ? (JSON.parse(payload) as unknown) : null this.emitter.emit(eventName, parsedPayload) } diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index db2e20ccc..46566b2c5 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -117,6 +117,9 @@ export class SketchupBridge extends BaseBridge { } private receiveResponse(requestId: string, data: string) { + console.log(requestId) + console.log(data) + if (!this.requests[requestId]) throw new Error( `Sketchup Bridge found no request to resolve with the id of ${requestId}. Something is weird!` diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 61eb51f5a..59fb3f1ee 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -5,6 +5,7 @@ import { IBaseBinding, IRhinoRandomBinding } from '~/lib/bindings/definitions/baseBindings' +import { SketchupBridge } from '~/lib/bridge/sketchup' // Makes TS happy declare let globalThis: Record & { @@ -25,10 +26,13 @@ export default defineNuxtPlugin(async () => { 'rhinoRandomBinding' ) + const sketchupRandomBinding = await tryHoistBinding('sketchupRandomBinding') + return { provide: { baseBinding, - rhinoRandomBinding + rhinoRandomBinding, + sketchupRandomBinding } } }) @@ -40,7 +44,7 @@ export default defineNuxtPlugin(async () => { * @returns null if the binding was not found, or the binding. */ const tryHoistBinding = async (name: string) => { - let bridge: GenericBridge | null = null + let bridge: GenericBridge | SketchupBridge | null = null if (globalThis.CefSharp) { await globalThis.CefSharp.BindObjectAsync(name) @@ -48,13 +52,15 @@ const tryHoistBinding = async (name: string) => { await bridge.create() } - if (globalThis.chrome && !bridge) { + if (globalThis.chrome && globalThis.chrome.webview && !bridge) { bridge = new GenericBridge(globalThis.chrome.webview.hostObjects[name]) await bridge.create() } if (globalThis.sketchup && !bridge) { - // TODO + bridge = new SketchupBridge(name) + const res = await bridge.isInitalized + if (!res) bridge = null } if (!bridge) console.warn(`Failed to bind ${name} binding.`) From 520a65c73cd9b899f08472e7a7aaee542bf41ab4 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Fri, 14 Jul 2023 11:13:21 +0100 Subject: [PATCH 16/35] experiments(dui3): cleanup --- .../dui3/lib/accounts/composables/setup.ts | 2 -- packages/dui3/lib/bridge/generic.ts | 12 +++++++++-- packages/dui3/lib/bridge/sketchup.ts | 10 +++++++++- packages/dui3/plugins/00.bindings.ts | 20 ++++++++++--------- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/dui3/lib/accounts/composables/setup.ts b/packages/dui3/lib/accounts/composables/setup.ts index 31339278f..c301f67c5 100644 --- a/packages/dui3/lib/accounts/composables/setup.ts +++ b/packages/dui3/lib/accounts/composables/setup.ts @@ -54,8 +54,6 @@ export async function useAccountsSetup(): Promise { accounts.value = newAccs } - // Call this one first to initialize the account state - // QUESTION: could be flopped in a iife so as not to block and drop the asyncness of this setup function? await refreshAccounts() const defaultAccount = computed(() => diff --git a/packages/dui3/lib/bridge/generic.ts b/packages/dui3/lib/bridge/generic.ts index a3afa7b1a..a09199c90 100644 --- a/packages/dui3/lib/bridge/generic.ts +++ b/packages/dui3/lib/bridge/generic.ts @@ -11,9 +11,15 @@ export class GenericBridge extends BaseBridge { this.bridge = object } - public async create() { + public async create(): Promise { // NOTE: GetMethods is a call to the .NET side. - const availableMethodNames = await this.bridge.GetBindingsMethodNames() + let availableMethodNames = [] as string[] + + try { + availableMethodNames = await this.bridge.GetBindingsMethodNames() + } catch { + return false + } // NOTE: hoisting original calls as lowerCasedMethodNames, but using the UpperCasedName for the .NET call // This allows us to follow js convetions and keep .NET ones too (eg. bindings.sayHi('') => public string SayHi(string name) {} @@ -23,6 +29,8 @@ export class GenericBridge extends BaseBridge { hoistTarget[lowercasedMethodName] = (...args: unknown[]) => this.runMethod(methodName, args) } + + return true } private async runMethod(methodName: string, args: unknown[]): Promise { diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index 46566b2c5..f32aa7342 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -47,7 +47,9 @@ export class SketchupBridge extends BaseBridge { // NOTE: we need to hoist the bindings in global scope BEFORE we call sketchup exec get comands below. ;(globalThis as Record).bindings = this + } + public async create(): Promise { // Initialization continues in the receiveCommandsAndInitializeBridge function, // where we expect sketchup to return to us the command names for related bindings/views. // NOTE: as we want to have multiple sketchup bindings in the future, we will @@ -55,6 +57,13 @@ export class SketchupBridge extends BaseBridge { // eslint-disable-next-line camelcase // sketchup.exec({ name: 'getCommands', view_id: this.bindingsName }) sketchup.getCommands(this.bindingsName) + + try { + await this.isInitalized + return true + } catch { + return false + } } /** @@ -79,7 +88,6 @@ export class SketchupBridge extends BaseBridge { * @param message */ private rejectBindings(message: string) { - this.resolveIsInitializedPromise(false) this.rejectIsInitializedPromise(message) } diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 59fb3f1ee..0a53352a4 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -22,6 +22,8 @@ declare let globalThis: Record & { export default defineNuxtPlugin(async () => { const baseBinding = await tryHoistBinding('baseBinding') + const nonExistantBindings = await tryHoistBinding('nonExistantBindings') + const rhinoRandomBinding = await tryHoistBinding( 'rhinoRandomBinding' ) @@ -45,24 +47,24 @@ export default defineNuxtPlugin(async () => { */ const tryHoistBinding = async (name: string) => { let bridge: GenericBridge | SketchupBridge | null = null + let tempBridge: GenericBridge | SketchupBridge | null = null if (globalThis.CefSharp) { await globalThis.CefSharp.BindObjectAsync(name) - bridge = new GenericBridge(globalThis[name] as unknown as IRawBridge) - await bridge.create() + tempBridge = new GenericBridge(globalThis[name] as unknown as IRawBridge) } - if (globalThis.chrome && globalThis.chrome.webview && !bridge) { - bridge = new GenericBridge(globalThis.chrome.webview.hostObjects[name]) - await bridge.create() + if (globalThis.chrome && globalThis.chrome.webview && !tempBridge) { + tempBridge = new GenericBridge(globalThis.chrome.webview.hostObjects[name]) } - if (globalThis.sketchup && !bridge) { - bridge = new SketchupBridge(name) - const res = await bridge.isInitalized - if (!res) bridge = null + if (globalThis.sketchup && !tempBridge) { + tempBridge = new SketchupBridge(name) } + const res = await tempBridge?.create() + if (res) bridge = tempBridge + if (!bridge) console.warn(`Failed to bind ${name} binding.`) globalThis[name] = bridge From 77aad568bce4e66dffc90445c1bcad095c62eee1 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Fri, 14 Jul 2023 11:28:17 +0100 Subject: [PATCH 17/35] experiments(dui3): added test bindings --- .../lib/bindings/definitions/baseBindings.ts | 29 +------------------ .../lib/bindings/definitions/testBindings.ts | 25 ++++++++++++++++ packages/dui3/plugins/00.bindings.ts | 21 +++++--------- 3 files changed, 33 insertions(+), 42 deletions(-) create mode 100644 packages/dui3/lib/bindings/definitions/testBindings.ts diff --git a/packages/dui3/lib/bindings/definitions/baseBindings.ts b/packages/dui3/lib/bindings/definitions/baseBindings.ts index d6170caa0..d59330531 100644 --- a/packages/dui3/lib/bindings/definitions/baseBindings.ts +++ b/packages/dui3/lib/bindings/definitions/baseBindings.ts @@ -1,8 +1,3 @@ -// Needs to be agreed between Frontend and Rhino -export interface IRhinoRandomBinding { - makeGreeting: (name: string) => Promise -} - // Needs to be agreed between Frontend and Core export interface IBaseBinding { getAccounts: () => Promise @@ -10,10 +5,6 @@ export interface IBaseBinding { getSourceApplicationVersion: () => Promise getDocumentInfo: () => Promise - // TODO: - getFileState: () => Promise - updateFileState: (state: FileState) => Promise - /** * Subscribe to messages from the host application. * @param event @@ -48,33 +39,15 @@ export type Account = { } } -export type FileState = { - models: ModelCard[] -} - -export type ModelCard = { - serverUrl: string // we need to select the correct account - modelId: string // we need to assemble the gql query properly - projectId: string - type: 'sender' | 'receiver' - status?: 'idle' | 'inprogress' | 'error' | 'warning' | 'disabled' | 'expired' //??? - // settings: Record??? - // report: Record??? - // progress: Record // ??? send status, receive status -} export type DocumentInfo = { location: string name: string id: string } +// NOTE: just a reminder for now export type ToastInfo = { text: string details?: string type: 'info' | 'error' | 'warning' } - -export type SelectionChangedInfo = { - objectIds: string[] - humanReadableSummary?: string -} diff --git a/packages/dui3/lib/bindings/definitions/testBindings.ts b/packages/dui3/lib/bindings/definitions/testBindings.ts new file mode 100644 index 000000000..8fc21ac35 --- /dev/null +++ b/packages/dui3/lib/bindings/definitions/testBindings.ts @@ -0,0 +1,25 @@ +export interface ITestBinding { + sayHi: (name: string, count: number, sayHelloNotHi: boolean) => Promise + goAway: () => Promise + getComplexType: () => Promise + on: ( + event: E, + callback: ITestBindingEvents[E] + ) => void +} + +export interface ITestBindingEvents { + emptyTestEvent: () => void + testEvent: (args: TestEventArgs) => void +} + +export type TestEventArgs = { + name: string + isOk: boolean + count: number +} + +export type ComplexType = { + id: string + count: number +} diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 0a53352a4..955a0ce90 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -1,11 +1,9 @@ import { GenericBridge } from '~/lib/bridge/generic' import { IRawBridge } from '~/lib/bridge/definitions' -import { - IBaseBinding, - IRhinoRandomBinding -} from '~/lib/bindings/definitions/baseBindings' +import { IBaseBinding } from '~/lib/bindings/definitions/baseBindings' import { SketchupBridge } from '~/lib/bridge/sketchup' +import { ITestBinding } from '~/lib/bindings/definitions/testBindings' // Makes TS happy declare let globalThis: Record & { @@ -20,21 +18,16 @@ declare let globalThis: Record & { * strip or customize functionality from the ui itself. */ export default defineNuxtPlugin(async () => { - const baseBinding = await tryHoistBinding('baseBinding') - + const testBindings = await tryHoistBinding('testBindings') const nonExistantBindings = await tryHoistBinding('nonExistantBindings') - const rhinoRandomBinding = await tryHoistBinding( - 'rhinoRandomBinding' - ) - - const sketchupRandomBinding = await tryHoistBinding('sketchupRandomBinding') + const baseBinding = await tryHoistBinding('baseBinding') return { provide: { - baseBinding, - rhinoRandomBinding, - sketchupRandomBinding + testBindings, + nonExistantBindings, + baseBinding } } }) From fd46eaebe6ecc674ce7edcb3a5d082b04a117edd Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Fri, 14 Jul 2023 11:39:47 +0100 Subject: [PATCH 18/35] experiments(dui3): wip ITestBinding + page --- .../dui3/lib/bindings/definitions/testBindings.ts | 1 + packages/dui3/pages/index.vue | 3 +++ packages/dui3/pages/test.vue | 11 +++++++++++ 3 files changed, 15 insertions(+) create mode 100644 packages/dui3/pages/test.vue diff --git a/packages/dui3/lib/bindings/definitions/testBindings.ts b/packages/dui3/lib/bindings/definitions/testBindings.ts index 8fc21ac35..500850fd2 100644 --- a/packages/dui3/lib/bindings/definitions/testBindings.ts +++ b/packages/dui3/lib/bindings/definitions/testBindings.ts @@ -2,6 +2,7 @@ export interface ITestBinding { sayHi: (name: string, count: number, sayHelloNotHi: boolean) => Promise goAway: () => Promise getComplexType: () => Promise + triggerEvent: (eventName: string) => Promise on: ( event: E, callback: ITestBindingEvents[E] diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index 6242797a3..45ffbf581 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -10,6 +10,9 @@ Hello world! You have {{ accounts.length }} accounts.
+
+ Go To Test Bindings Page +
{{ acc.accountInfo.userInfo.email }} @ diff --git a/packages/dui3/pages/test.vue b/packages/dui3/pages/test.vue new file mode 100644 index 000000000..383c8e994 --- /dev/null +++ b/packages/dui3/pages/test.vue @@ -0,0 +1,11 @@ + + From 8597c3243429aab0aa576b344544aa8244881a52 Mon Sep 17 00:00:00 2001 From: oguzhankoral Date: Fri, 14 Jul 2023 15:59:07 +0300 Subject: [PATCH 19/35] Add testBindings functions -validated for sketchup --- packages/dui3/lib/bridge/base.ts | 4 +- packages/dui3/lib/bridge/sketchup.ts | 10 +++-- packages/dui3/pages/test.vue | 59 +++++++++++++++++++++++++--- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/packages/dui3/lib/bridge/base.ts b/packages/dui3/lib/bridge/base.ts index 8d0e1e67f..04c49ef60 100644 --- a/packages/dui3/lib/bridge/base.ts +++ b/packages/dui3/lib/bridge/base.ts @@ -5,7 +5,7 @@ import { createNanoEvents, Emitter } from 'nanoevents' * e.g. via `browser.executeScriptAsync("myBindings.on('eventName', serializedData)")`. */ export class BaseBridge { - private emitter: Emitter + public emitter: Emitter constructor() { this.emitter = createNanoEvents() @@ -18,8 +18,6 @@ export class BaseBridge { // NOTE: this could be private - as it should be only used by the host application. emit(eventName: string, payload: string) { - console.log(payload) - const parsedPayload = payload ? (JSON.parse(payload) as unknown) : null this.emitter.emit(eventName, parsedPayload) } diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index f32aa7342..79f522d60 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -49,6 +49,13 @@ export class SketchupBridge extends BaseBridge { ;(globalThis as Record).bindings = this } + // NOTE: Overriden function for now, need to be checked later payload can be unified or not. + // From sketchup we receive beatiful JSON object that we do not need to parse. + // This should be checked with .NET + emit(eventName: string, payload: string): void { + this.emitter.emit(eventName, payload) + } + public async create(): Promise { // Initialization continues in the receiveCommandsAndInitializeBridge function, // where we expect sketchup to return to us the command names for related bindings/views. @@ -125,9 +132,6 @@ export class SketchupBridge extends BaseBridge { } private receiveResponse(requestId: string, data: string) { - console.log(requestId) - console.log(data) - if (!this.requests[requestId]) throw new Error( `Sketchup Bridge found no request to resolve with the id of ${requestId}. Something is weird!` diff --git a/packages/dui3/pages/test.vue b/packages/dui3/pages/test.vue index 383c8e994..1e002b42a 100644 --- a/packages/dui3/pages/test.vue +++ b/packages/dui3/pages/test.vue @@ -1,11 +1,58 @@ - + + From 9d812e935d01427c1f856faf2252eed38f34fc39 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Sat, 15 Jul 2023 18:10:50 +0100 Subject: [PATCH 20/35] experiments(dui3): cleanup --- packages/dui3/components/header/NavBar.vue | 10 ++-------- .../lib/bindings/definitions/baseBindings.ts | 6 ++++++ .../lib/bindings/definitions/testBindings.ts | 9 +++++++++ packages/dui3/lib/bridge/generic.ts | 13 ++++++++++++- packages/dui3/lib/bridge/sketchup.ts | 18 ++++++++++-------- packages/dui3/pages/index.vue | 3 ++- packages/dui3/pages/test.vue | 10 +++------- packages/dui3/plugins/00.bindings.ts | 15 +++++++++++---- 8 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/dui3/components/header/NavBar.vue b/packages/dui3/components/header/NavBar.vue index 097e23e17..6d28e7bad 100644 --- a/packages/dui3/components/header/NavBar.vue +++ b/packages/dui3/components/header/NavBar.vue @@ -7,14 +7,8 @@
- - + Show Dev Tools
@@ -24,5 +18,5 @@ diff --git a/packages/dui3/lib/bindings/definitions/baseBindings.ts b/packages/dui3/lib/bindings/definitions/baseBindings.ts index d59330531..34961ef18 100644 --- a/packages/dui3/lib/bindings/definitions/baseBindings.ts +++ b/packages/dui3/lib/bindings/definitions/baseBindings.ts @@ -14,6 +14,11 @@ export interface IBaseBinding { event: E, callback: IBaseBindingHostEvents[E] ) => void + /** + * Note: this method does not need to be implemented in the .NET host application base bindings, + * it is served by the DUI3 bridge. + */ + showDevTools: () => Promise } export interface IBaseBindingHostEvents { @@ -21,6 +26,7 @@ export interface IBaseBindingHostEvents { documentChanged: () => void } +// An almost 1-1 mapping of what we need from the Core accounts class. export type Account = { id: string isDefault: boolean diff --git a/packages/dui3/lib/bindings/definitions/testBindings.ts b/packages/dui3/lib/bindings/definitions/testBindings.ts index 500850fd2..c2aae246d 100644 --- a/packages/dui3/lib/bindings/definitions/testBindings.ts +++ b/packages/dui3/lib/bindings/definitions/testBindings.ts @@ -1,7 +1,16 @@ +/** + * The name under which this binding will be registered. + */ +export const ITestBindingKey = 'testBinding' + +/** + * A test binding interface to ensure compatbility. Ideally all host environments would implement and register it. + */ export interface ITestBinding { sayHi: (name: string, count: number, sayHelloNotHi: boolean) => Promise goAway: () => Promise getComplexType: () => Promise + shouldThrow: () => Promise triggerEvent: (eventName: string) => Promise on: ( event: E, diff --git a/packages/dui3/lib/bridge/generic.ts b/packages/dui3/lib/bridge/generic.ts index a09199c90..b4c22c550 100644 --- a/packages/dui3/lib/bridge/generic.ts +++ b/packages/dui3/lib/bridge/generic.ts @@ -35,13 +35,24 @@ export class GenericBridge extends BaseBridge { private async runMethod(methodName: string, args: unknown[]): Promise { const preserializedArgs = args.map((a) => JSON.stringify(a)) + // NOTE: RunMethod is a call to the .NET side. const result = await this.bridge.RunMethod( methodName, JSON.stringify(preserializedArgs) ) - return JSON.parse(result) as unknown + const parsed = result ? (JSON.parse(result) as Record) : null + + if (parsed && parsed['error']) { + throw new Error(parsed['error'] as string) + } + + return parsed + } + + public showDevTools() { + this.bridge.ShowDevTools() } } diff --git a/packages/dui3/lib/bridge/sketchup.ts b/packages/dui3/lib/bridge/sketchup.ts index 79f522d60..75fb28c8c 100644 --- a/packages/dui3/lib/bridge/sketchup.ts +++ b/packages/dui3/lib/bridge/sketchup.ts @@ -49,22 +49,17 @@ export class SketchupBridge extends BaseBridge { ;(globalThis as Record).bindings = this } - // NOTE: Overriden function for now, need to be checked later payload can be unified or not. - // From sketchup we receive beatiful JSON object that we do not need to parse. - // This should be checked with .NET + // NOTE: Overriden emit as we do not need to parse the data back - the Sketchup bridge already parses it for us. emit(eventName: string, payload: string): void { - this.emitter.emit(eventName, payload) + this.emitter.emit(eventName, payload as unknown as Record) } public async create(): Promise { // Initialization continues in the receiveCommandsAndInitializeBridge function, // where we expect sketchup to return to us the command names for related bindings/views. - // NOTE: as we want to have multiple sketchup bindings in the future, we will - // most likely change this method to specify which view/plugin/bindings we want. - // eslint-disable-next-line camelcase - // sketchup.exec({ name: 'getCommands', view_id: this.bindingsName }) sketchup.getCommands(this.bindingsName) + // try { await this.isInitalized return true @@ -148,4 +143,11 @@ export class SketchupBridge extends BaseBridge { delete this.requests[requestId] } } + + public showDevTools() { + // eslint-disable-next-line no-alert + window.alert( + 'Sketchup cannot do this. The dev tools menu is accessible via a right click.' + ) + } } diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index 45ffbf581..cdf848732 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -44,9 +44,10 @@ import { graphql } from '~/lib/common/generated/gql' import { ServerInfoTestQuery } from '~/lib/common/generated/gql/graphql' import { useDocumentInfoSetup } from '~/lib/document-info' +const { accounts, refreshAccounts, defaultAccount } = await useAccountsSetup() + const { $baseBinding } = useNuxtApp() const appName = await $baseBinding.getSourceApplicationName() -const { accounts, refreshAccounts, defaultAccount } = await useAccountsSetup() const documentInfo = await useDocumentInfoSetup() diff --git a/packages/dui3/pages/test.vue b/packages/dui3/pages/test.vue index 1e002b42a..495af4489 100644 --- a/packages/dui3/pages/test.vue +++ b/packages/dui3/pages/test.vue @@ -21,16 +21,12 @@ import { ComplexType, TestEventArgs } from '~/lib/bindings/definitions/testBindi const { $testBindings } = useNuxtApp() $testBindings.on('emptyTestEvent', () => { - setTimeout(() => { - console.log('empty test event catched!') - }, 500) + console.log('empty test event catched!') }) $testBindings.on('testEvent', (args: TestEventArgs) => { - setTimeout(() => { - console.log('test event catched!') - console.log(args) - }, 500) + console.log('test event catched!') + console.log(args) }) async function triggerEmptyEvent() { diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 955a0ce90..1fd7827bb 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -3,7 +3,7 @@ import { IRawBridge } from '~/lib/bridge/definitions' import { IBaseBinding } from '~/lib/bindings/definitions/baseBindings' import { SketchupBridge } from '~/lib/bridge/sketchup' -import { ITestBinding } from '~/lib/bindings/definitions/testBindings' +import { ITestBinding, ITestBindingKey } from '~/lib/bindings/definitions/testBindings' // Makes TS happy declare let globalThis: Record & { @@ -18,16 +18,23 @@ declare let globalThis: Record & { * strip or customize functionality from the ui itself. */ export default defineNuxtPlugin(async () => { - const testBindings = await tryHoistBinding('testBindings') + // Registers some default test bindings. + const testBindings = await tryHoistBinding(ITestBindingKey) + // Tries to register some non-existant bindings. const nonExistantBindings = await tryHoistBinding('nonExistantBindings') - + // Registers a set of default bindings. const baseBinding = await tryHoistBinding('baseBinding') + const showDevTools = () => { + baseBinding.showDevTools() + } + return { provide: { testBindings, nonExistantBindings, - baseBinding + baseBinding, + showDevTools } } }) From d176d0c59602f93a272719870b377afbbe3a375c Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Sun, 16 Jul 2023 09:42:40 +0100 Subject: [PATCH 21/35] experiments(dui3): poor man's test runniner in /test --- packages/dui3/layouts/default.vue | 2 +- packages/dui3/lib/bridge/generic.ts | 7 +- packages/dui3/pages/test.vue | 178 +++++++++++++++++++++------- 3 files changed, 145 insertions(+), 42 deletions(-) diff --git a/packages/dui3/layouts/default.vue b/packages/dui3/layouts/default.vue index 8c8905991..47180542b 100644 --- a/packages/dui3/layouts/default.vue +++ b/packages/dui3/layouts/default.vue @@ -1,7 +1,7 @@ diff --git a/packages/dui3/components/user/Avatar.vue b/packages/dui3/components/user/Avatar.vue new file mode 100644 index 000000000..a5821f3fc --- /dev/null +++ b/packages/dui3/components/user/Avatar.vue @@ -0,0 +1,10 @@ + + diff --git a/packages/dui3/lib/accounts/composables/setup.ts b/packages/dui3/lib/accounts/composables/setup.ts index c301f67c5..661b0383b 100644 --- a/packages/dui3/lib/accounts/composables/setup.ts +++ b/packages/dui3/lib/accounts/composables/setup.ts @@ -18,8 +18,10 @@ export type DUIAccountsState = { } const AccountsInjectionKey = 'DUI_ACCOUNTS_STATE' - -export async function useAccountsSetup(): Promise { +/** + * Use this composable to set up the account bindings and graphql clients at the top of the app. + */ +export function useAccountsSetup(): DUIAccountsState { const app = useNuxtApp() const $baseBinding = app.$baseBinding @@ -31,6 +33,8 @@ export async function useAccountsSetup(): Promise { // Matches local accounts coming from the host app to app state. const refreshAccounts = async () => { const accs = await $baseBinding.getAccounts() + // We create a whole new list of accounts that will replace the old list. This way we ensure we drop + // out of scope old accounts that not exist anymore (TODO: test), and we don't need to do complex diffing. const newAccs = [] as DUIAccount[] for (const acc of accs) { const existing = accounts.value.find((a) => a.accountInfo.id === acc.id) @@ -45,7 +49,9 @@ export async function useAccountsSetup(): Promise { authToken: () => acc.token }) ) + apolloClients[acc.id] = client + newAccs.push({ accountInfo: acc, client @@ -54,7 +60,7 @@ export async function useAccountsSetup(): Promise { accounts.value = newAccs } - await refreshAccounts() + ;(async () => await refreshAccounts())() const defaultAccount = computed(() => accounts.value.find((acc) => acc.accountInfo.isDefault) @@ -71,6 +77,9 @@ export async function useAccountsSetup(): Promise { return accState } +/** + * Use this composable to access the users' local accounts and their corresponding graphql client. + */ export function useInjectedAccounts(): DUIAccountsState { const state = inject(AccountsInjectionKey) as DUIAccountsState return state diff --git a/packages/dui3/lib/document-info/index.ts b/packages/dui3/lib/document-info/index.ts index ff7f00118..a65b89c57 100644 --- a/packages/dui3/lib/document-info/index.ts +++ b/packages/dui3/lib/document-info/index.ts @@ -1,20 +1,19 @@ import { DocumentInfo } from '~/lib/bindings/definitions/baseBindings' import { ref } from 'vue' -const DocumentInfoInjectionKey = 'DUI_ACCOUNTS_STATE' +const DocumentInfoInjectionKey = 'DUI_DOC_INFO_STATE' -export async function useDocumentInfoSetup() { +export function useDocumentInfoSetup() { const app = useNuxtApp() const documentInfo = ref() - app.$baseBinding.on('documentChanged', () => { + app.$baseBinding?.on('documentChanged', () => { setTimeout(async () => { const docInfo = await app.$baseBinding.getDocumentInfo() documentInfo.value = docInfo - }, 500) // Don't ask + }, 500) // Rhino needs some time. }) - - documentInfo.value = await app.$baseBinding.getDocumentInfo() + ;(async () => (documentInfo.value = await app.$baseBinding.getDocumentInfo()))() provide(DocumentInfoInjectionKey, documentInfo) return documentInfo } diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index cdf848732..bf286ca1d 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -39,17 +39,17 @@ diff --git a/packages/dui3/components/user/Avatar.vue b/packages/dui3/components/user/Avatar.vue index a5821f3fc..a21020dce 100644 --- a/packages/dui3/components/user/Avatar.vue +++ b/packages/dui3/components/user/Avatar.vue @@ -1,10 +1,162 @@ diff --git a/packages/dui3/lib/accounts/composables/setup.ts b/packages/dui3/lib/accounts/composables/setup.ts index 03454433b..d9e4e46cc 100644 --- a/packages/dui3/lib/accounts/composables/setup.ts +++ b/packages/dui3/lib/accounts/composables/setup.ts @@ -1,7 +1,7 @@ import { ApolloClient } from '@apollo/client/core' import { ApolloClients } from '@vue/apollo-composable' import { ShallowRef, ComputedRef } from 'vue' -import { Account } from '~/lib/bindings/definitions/baseBindings' +import { Account } from '~/lib/bindings/definitions/IBasicConnectorBinding' import { resolveClientConfig } from '~/lib/core/configs/apollo' export type DUIAccount = { diff --git a/packages/dui3/pages/index.vue b/packages/dui3/pages/index.vue index bf286ca1d..5d62e4cf8 100644 --- a/packages/dui3/pages/index.vue +++ b/packages/dui3/pages/index.vue @@ -1,10 +1,5 @@ - + diff --git a/packages/dui3/components/header/UserMenu.vue b/packages/dui3/components/header/UserMenu.vue index 2ad7d4887..161d0ec90 100644 --- a/packages/dui3/components/header/UserMenu.vue +++ b/packages/dui3/components/header/UserMenu.vue @@ -45,6 +45,17 @@ Test Page + + + Show Dev Tools + + Hai @@ -52,14 +63,16 @@
diff --git a/packages/dui3/components/header/UserMenu.vue b/packages/dui3/components/header/UserMenu.vue index 161d0ec90..9a3093a9c 100644 --- a/packages/dui3/components/header/UserMenu.vue +++ b/packages/dui3/components/header/UserMenu.vue @@ -20,57 +20,64 @@ - - - Home - + +
+
Loading accounts...
+
+
Your accounts
+
+ + Refresh + +
+
+
+ +
+
- - - - Test Page - + +
+ + + Test Page + +
- - - Show Dev Tools - - - Hai
+ diff --git a/packages/dui3/plugins/00.bindings.ts b/packages/dui3/plugins/00.bindings.ts index 3451380bf..b6df5669f 100644 --- a/packages/dui3/plugins/00.bindings.ts +++ b/packages/dui3/plugins/00.bindings.ts @@ -15,6 +15,12 @@ import { MockedTestBinding } from '~/lib/bindings/definitions/ITestBinding' +import { + IConfigBinding, + IConfigBindingKey, + MockedConfigBinding +} from '~/lib/bindings/definitions/IConfigBinding' + // Makes TS happy declare let globalThis: Record & { CefSharp?: { BindObjectAsync: (name: string) => Promise } @@ -40,6 +46,9 @@ export default defineNuxtPlugin(async () => { (await tryHoistBinding(IBasicConnectorBindingKey)) || new MockedBaseBinding() + // UI configuration bindings. + const configBinding = await tryHoistBinding(IConfigBindingKey) + const showDevTools = () => { baseBinding.showDevTools() } @@ -53,6 +62,7 @@ export default defineNuxtPlugin(async () => { testBindings, nonExistantBindings, baseBinding, + configBinding, showDevTools, openUrl } diff --git a/packages/dui3/store/uiConfig.ts b/packages/dui3/store/uiConfig.ts new file mode 100644 index 000000000..404d8ae56 --- /dev/null +++ b/packages/dui3/store/uiConfig.ts @@ -0,0 +1,34 @@ +import { defineStore } from 'pinia' +import { Config } from 'lib/bindings/definitions/IConfigBinding' + +export const useDocumentInfoStore = defineStore('documentInfoStore', () => { + const { $configBinding } = useNuxtApp() + + const hasConfigBindings = ref(!!$configBinding) + const uiConfig = ref({ darkTheme: false }) + + watch( + uiConfig, + async (newValue) => { + if (!newValue || !$configBinding) return + await $configBinding.updateConfig(newValue) + }, + { deep: true } + ) + + const isDarkTheme = computed(() => { + return uiConfig.value?.darkTheme + }) + + const toggleTheme = () => { + uiConfig.value.darkTheme = !uiConfig.value.darkTheme + } + + const init = async () => { + if (!$configBinding) return + uiConfig.value = await $configBinding.getConfig() + } + void init() + + return { hasConfigBindings, isDarkTheme, toggleTheme } +}) From 98029dbfce15207301422d7ea01c662733eec5a1 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Sun, 23 Jul 2023 20:40:41 +0100 Subject: [PATCH 35/35] feat(dui3): minor layout fix --- packages/dui3/components/header/UserMenu.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dui3/components/header/UserMenu.vue b/packages/dui3/components/header/UserMenu.vue index 321462afa..a7881e4f1 100644 --- a/packages/dui3/components/header/UserMenu.vue +++ b/packages/dui3/components/header/UserMenu.vue @@ -45,7 +45,7 @@
-
+