feat: introduce CI linting & fix various issues (#5)

* introduce CI checks

* fixx

* add caching

* fixes

* wip

* server bridge linting

* No lint errors

* fix paths on lint:prettier

* make files pretty again

* fix stylelint

* fix lock

---------

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
This commit is contained in:
Oğuzhan Koral
2025-05-14 10:05:51 +03:00
committed by GitHub
parent f70915f485
commit fe77ede49e
39 changed files with 1127 additions and 1563 deletions
-1
View File
File diff suppressed because one or more lines are too long
+44
View File
@@ -0,0 +1,44 @@
name: Linting
on:
pull_request:
branches:
- main
jobs:
lint-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.14.0'
- name: Enable Corepack and Install Correct Yarn Version
run: |
corepack enable
corepack prepare yarn@$(jq -r .packageManager package.json | cut -d'@' -f2) --activate
yarn --version
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
**/node_modules
.yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies
run: yarn install --immutable
- name: Run Linter
run: yarn lint
- name: Run generate
run: yarn generate
+35
View File
@@ -0,0 +1,35 @@
node_modules
build
dist
dist2
dist-*
coverage
.nyc_output
.output
.nuxt
**/nuxt-modules/**/templates/*.js
/lib/common/generated/**/*
package-lock.json
yarn.lock
.yarn
# Profiler output
events.json
# Prettier doesn't understand the syntax inside the Yaml files, because of the brackets
utils/helm/speckle-server/templates
# Optional eslint cache
.eslintcache
.venv
venv
.*.{ts,js,vue,tsx,jsx}
**/generated/**/*
**/generated/graphql.ts
storybook-static
.tshy
.tshy-build
+11
View File
@@ -0,0 +1,11 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"endOfLine": "auto",
"bracketSpacing": true,
"vueIndentScriptAndStyle": false,
"htmlWhitespaceSensitivity": "ignore",
"printWidth": 88,
"singleQuote": true
}
+16 -16
View File
@@ -1,18 +1,18 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"Vue.volar",
"bradlc.vscode-tailwindcss",
"stylelint.vscode-stylelint",
"cpylua.language-postcss",
"graphql.vscode-graphql",
"graphql.vscode-graphql-syntax"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": ["octref.vetur"]
}
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"Vue.volar",
"bradlc.vscode-tailwindcss",
"stylelint.vscode-stylelint",
"cpylua.language-postcss",
"graphql.vscode-graphql",
"graphql.vscode-graphql-syntax"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": ["octref.vetur"]
}
+1 -1
View File
@@ -1 +1 @@
nodeLinker: node-modules
nodeLinker: node-modules
-1
View File
@@ -52,4 +52,3 @@ onMounted(() => {
})
})
</script>
<style></style>
+2 -1
View File
@@ -1,5 +1,6 @@
/* stylelint-disable selector-id-pattern */
@import '@speckle/ui-components/style.css';
@import url('@speckle/ui-components/style.css');
@tailwind base;
@tailwind components;
@tailwind utilities;
+1 -1
View File
@@ -60,7 +60,7 @@
<script setup lang="ts">
import type { DUIAccount } from '~~/store/accounts'
import { TrashIcon } from '@heroicons/vue/24/outline'
import { type BaseBridge } from '~/lib/bridge/base'
import type { BaseBridge } from '~/lib/bridge/base'
const { $accountBinding } = useNuxtApp()
+4 -3
View File
@@ -20,7 +20,7 @@
mount-menu-on-body
>
<template #something-selected="{ value }">
<span>{{ value.name }}</span>
<span>{{ isArray(value) ? value[0].name : value.name }}</span>
</template>
<template #option="{ item }">
<div class="flex items-center">
@@ -80,10 +80,11 @@ import {
createAutomationMutation
} from '~/lib/graphql/mutationsAndQueries'
import { provideApolloClient, useMutation, useQuery } from '@vue/apollo-composable'
import { useAccountStore } from '~/store/accounts'
import { useAccountStore, type DUIAccount } from '~/store/accounts'
import type { ApolloError } from '@apollo/client/errors'
import { formatVersionParams } from '~/lib/common/helpers/jsonSchema'
import { useJsonFormsChangeHandler } from '~/lib/core/composables/jsonSchema'
import { isArray } from 'lodash-es'
const props = defineProps<{
projectId: string
@@ -106,7 +107,7 @@ const toggleDialog = () => {
showAutomateDialog.value = !showAutomateDialog.value
}
const { mutate } = provideApolloClient(activeAccount.value.client)(() =>
const { mutate } = provideApolloClient((activeAccount.value as DUIAccount).client)(() =>
useMutation(createAutomationMutation)
)
+1
View File
@@ -329,6 +329,7 @@ onUnmounted(() => {
html.dialog-open {
overflow: visible !important;
}
html.dialog-open body {
overflow: hidden !important;
}
+20 -2
View File
@@ -19,7 +19,7 @@
<FormButton
v-if="notification.cta"
size="sm"
:color="notification.level === 'info' ? 'outline' : notification.level"
:color="notificationButtonColor(notification.level)"
full-width
@click.stop="notification.cta?.action"
>
@@ -47,7 +47,10 @@
<script setup lang="ts">
import { useTimeoutFn } from '@vueuse/core'
import type { ModelCardNotification } from '~/lib/models/card/notification'
import type {
ModelCardNotification,
ModelCardNotificationLevel
} from '~/lib/models/card/notification'
import { XMarkIcon } from '@heroicons/vue/24/outline'
const props = defineProps<{
notification: ModelCardNotification
@@ -59,6 +62,21 @@ if (props.notification.timeout) {
useTimeoutFn(() => emit('dismiss'), props.notification.timeout)
}
const notificationButtonColor = (notificationLevel: ModelCardNotificationLevel) => {
switch (notificationLevel) {
case 'info':
return 'outline'
case 'danger':
return 'danger'
case 'success':
return 'primary'
case 'warning':
return 'danger'
default:
return 'outline'
}
}
const textClassColor = computed(() => {
switch (props.notification.level) {
case 'danger':
+1 -1
View File
@@ -8,7 +8,7 @@
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import type { IDirectSelectionSendFilter, ISendFilter } from 'lib/models/card/send'
import type { IDirectSelectionSendFilter, ISendFilter } from '~/lib/models/card/send'
import { useHostAppStore } from '~~/store/hostApp'
import { useSelectionStore } from '~~/store/selection'
+4 -2
View File
@@ -159,7 +159,7 @@
>
<UserAvatarGroup
size="xs"
:users="[latestCommentNotification.comment?.author]"
:users="[latestCommentNotification.comment?.author as AvatarUserWithId]"
/>
<span class="line-clamp-1">
{{ latestCommentNotification.comment?.author.name }} just left a
@@ -208,6 +208,7 @@ import { useIntervalFn, useTimeoutFn } from '@vueuse/core'
import type { ProjectCommentsUpdatedMessage } from '~/lib/common/generated/gql/graphql'
import { useFunctionRunsStatusSummary } from '~/lib/automate/runStatus'
import { CursorArrowRaysIcon, XCircleIcon } from '@heroicons/vue/24/outline'
import type { AvatarUserWithId } from '@speckle/ui-components'
const app = useNuxtApp()
const store = useHostAppStore()
@@ -430,7 +431,8 @@ const { start: startCommentClearTimeout, stop: stopCommentClearTimeout } = useTi
)
onCommentResult((res) => {
latestCommentNotification.value = res.data?.projectCommentsUpdated
latestCommentNotification.value = res.data
?.projectCommentsUpdated as ProjectCommentsUpdatedMessage
startCommentClearTimeout()
})
+1 -1
View File
@@ -75,7 +75,7 @@ import {
} from '@heroicons/vue/24/solid'
import type { ConversionResult } from '~/lib/conversions/conversionResult'
import { useAccountStore } from '~/store/accounts'
import type { IModelCard } from 'lib/models/card'
import type { IModelCard } from '~/lib/models/card'
import { useHostAppStore } from '~/store/hostApp'
const app = useNuxtApp()
+16 -4
View File
@@ -149,7 +149,7 @@ import { useMutation, provideApolloClient, useQuery } from '@vue/apollo-composab
import type {
ProjectListProjectItemFragment,
WorkspaceListWorkspaceItemFragment
} from 'lib/common/generated/gql/graphql'
} from '~/lib/common/generated/gql/graphql'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { useConfigStore } from '~/store/config'
@@ -210,7 +210,11 @@ const handleProjectCreated = (result: ProjectListProjectItemFragment) => {
const { result: serverInfoResult, refetch: refetchServerInfo } = useQuery(
serverInfoQuery,
() => ({}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: 'network-only'
})
)
const workspacesEnabled = computed(
@@ -222,7 +226,11 @@ const { result: workspacesResult, refetch: refetchWorkspaces } = useQuery(
() => ({
limit: 100
}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: 'network-only'
})
)
const workspaces = computed(() => workspacesResult.value?.activeUser?.workspaces.items)
@@ -230,7 +238,11 @@ const workspaces = computed(() => workspacesResult.value?.activeUser?.workspaces
const { result: activeWorkspaceResult, refetch: refetchActiveWorkspace } = useQuery(
activeWorkspaceQuery,
() => ({}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: 'network-only'
})
)
const activeWorkspace = computed(() => {
+8 -3
View File
@@ -13,7 +13,7 @@
mount-menu-on-body
>
<template #something-selected="{ value }">
<span>{{ value.name }}</span>
<span>{{ isArray(value) ? value[0].name : value.name }}</span>
</template>
<template #option="{ item }">
<div
@@ -44,9 +44,10 @@
import { ref, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { workspacesListQuery } from '~/lib/graphql/mutationsAndQueries'
import type { WorkspaceListWorkspaceItemFragment } from 'lib/common/generated/gql/graphql'
import type { WorkspaceListWorkspaceItemFragment } from '~/lib/common/generated/gql/graphql'
import { storeToRefs } from 'pinia'
import { useAccountStore } from '~/store/accounts'
import { isArray } from 'lodash-es'
const emit = defineEmits<{
(
@@ -69,7 +70,11 @@ const { result: workspacesResult } = useQuery(
search: (searchText.value || '').trim() || null
}
}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: 'network-only'
})
)
const workspaces = computed(() => workspacesResult.value?.activeUser?.workspaces.items)
+1 -1
View File
@@ -5,7 +5,7 @@
>
<UserAvatar
v-tippy="`Authored by ${version.authorUser?.name}`"
:user="version.authorUser"
:user="{ avatar: version.authorUser?.avatar, name: version.authorUser?.name as string }"
size="sm"
class="absolute inset-1"
/>
+53 -7
View File
@@ -1,7 +1,19 @@
import { omit } from 'lodash-es'
import { baseConfigs, globals, getESMDirname } from '../../eslint.config.mjs'
import withNuxt from './.nuxt/eslint.config.mjs'
import pluginVueA11y from 'eslint-plugin-vuejs-accessibility'
import globals from 'globals'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
import prettierConfig from 'eslint-config-prettier'
import js from '@eslint/js'
/**
* Feed in import.meta.url in your .mjs module to get the equivalent of __dirname
* @param {string} importMetaUrl
*/
export const getESMDirname = (importMetaUrl) => {
return dirname(fileURLToPath(importMetaUrl))
}
const configs = await withNuxt([
{
@@ -62,6 +74,7 @@ const configs = await withNuxt([
'@typescript-eslint/require-await': 'error',
'no-undef': 'off',
'@typescript-eslint/no-empty-object-type': 'off', // too restrictive
'@typescript-eslint/unified-signatures': 'off', // DX sucks in vue event definitions
'@typescript-eslint/no-dynamic-delete': 'off', // too restrictive
'@typescript-eslint/restrict-template-expressions': 'off', // too restrictive
@@ -78,10 +91,7 @@ const configs = await withNuxt([
{
files: ['**/*.vue'],
rules: {
'vue/component-tags-order': [
'error',
{ order: ['docs', 'template', 'script', 'style'] }
],
'vue/block-order': ['error', { order: ['docs', 'template', 'script', 'style'] }],
'vue/require-default-prop': 'off',
'vue/multi-word-component-names': 'off',
'vue/component-name-in-template-casing': [
@@ -116,10 +126,46 @@ const configs = await withNuxt([
'./lib/common/generated/**/*',
'storybook-static',
'.nuxt/**',
'.output/**'
'.output/**',
'**/dist/**',
'**/dist-*/**',
'**/public/**',
'**/events.json',
'**/generated/**/*'
]
},
...baseConfigs
{
files: ['**/*.mjs'],
languageOptions: {
sourceType: 'module'
}
},
{
files: ['**/*.cjs'],
languageOptions: {
sourceType: 'commonjs'
}
},
{
files: ['**/*.{js,mjs,cjs}', '**/.*.{js,mjs,cjs}'],
...js.configs.recommended
},
prettierConfig,
{
rules: {
camelcase: [
1,
{
properties: 'always'
}
],
'no-var': 'error',
'no-alert': 'error',
eqeqeq: 'error',
'prefer-const': 'warn',
'object-shorthand': 'warn'
}
}
])
export default configs
-136
View File
@@ -1,136 +0,0 @@
import { ApolloClient, gql } from '@apollo/client/core'
import { ApolloClients } from '@vue/apollo-composable'
import type { ComputedRef, Ref } from 'vue'
import type { Account } from '~/lib/bindings/definitions/IBasicConnectorBinding'
import { resolveClientConfig } from '~/lib/core/configs/apollo'
export type DUIAccount = {
/** account info coming from the host app */
accountInfo: Account
/** the graphql client; a bit superflous */
client?: ApolloClient<unknown>
/** whether an intial serverinfo query succeeded. */
isValid: boolean
}
export type DUIAccountsState = {
accounts: Ref<DUIAccount[]>
validAccounts: ComputedRef<DUIAccount[]>
refreshAccounts: () => Promise<void>
defaultAccount: ComputedRef<DUIAccount | undefined>
loading: Ref<boolean>
}
const AccountsInjectionKey = 'DUI_ACCOUNTS_STATE'
/**
* Use this composable to set up the account bindings and graphql clients at the top of the app.
* TODO: Properly handle cases when user was not connected to the internet,
* and then actually got connected.
*/
export function useAccountsSetup(): DUIAccountsState {
const app = useNuxtApp()
const $baseBinding = app.$baseBinding
const accounts = ref<DUIAccount[]>([])
const apolloClients = {} as Record<string, ApolloClient<unknown>>
// Tries to connect to the accounts and sets their is valid prop to false if fails.
const testAccounts = async (accs: DUIAccount[]) => {
const accountTestQuery = gql`
query AcccountTestQuery {
serverInfo {
version
name
company
}
}
`
for (const acc of accs) {
if (!acc.client) continue
try {
await acc.client.query({ query: accountTestQuery })
acc.isValid = true
} catch {
// TODO: properly dispose and kill this client. It's unclear how to do it.
acc.isValid = false
// NOTE: we do not want to delete the client, as we might want to "refresh" in
// case the user was not connected to the interweb.
// acc.client.disableNetworkFetches = true
// acc.client.stop()
// delete acc.client
}
}
}
const loading = ref(false)
// Matches local accounts coming from the host app to app state.
const refreshAccounts = async () => {
loading.value = true
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)
if (existing) {
newAccs.push(existing as DUIAccount)
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,
isValid: true
})
}
// We test accounts here so we try to prevent the app from querying/using invalid accounts.
await testAccounts(newAccs)
// Once we have tested the new accounts, finally set them.
accounts.value = newAccs
loading.value = false
}
void refreshAccounts() // Promise that we do not want to await (convention with void)
const defaultAccount = computed(() =>
accounts.value.find((acc) => acc.accountInfo.isDefault)
)
const validAccounts = computed(() => {
return accounts.value.filter((a) => a.isValid)
})
const accState = {
accounts,
defaultAccount,
validAccounts,
refreshAccounts,
loading
}
app.vueApp.provide(ApolloClients, apolloClients)
provide(AccountsInjectionKey, accState)
return accState // as DUIAccountsState
}
/**
* 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
}
+4 -1
View File
@@ -1,4 +1,7 @@
import type { IBinding, IBindingSharedEvents } from 'lib/bindings/definitions/IBinding'
import type {
IBinding,
IBindingSharedEvents
} from '~/lib/bindings/definitions/IBinding'
export const IAccountBindingKey = 'accountsBinding'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { ConversionResult } from '~/lib/conversions/conversionResult'
import type { IModelCardSharedEvents } from '~/lib/models/card'
import type { CardSetting } from '~/lib/models/card/setting'
import type {
+1 -1
View File
@@ -5,7 +5,7 @@ import type {
} from '~~/lib/bindings/definitions/IBinding'
import type { CardSetting } from '~/lib/models/card/setting'
import type { IModelCardSharedEvents } from '~/lib/models/card'
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { ConversionResult } from '~/lib/conversions/conversionResult'
import type { CreateVersionArgs } from '~/lib/bridge/server'
export const ISendBindingKey = 'sendBinding'
-156
View File
@@ -1,156 +0,0 @@
import { ArchicadBridge } from '~/lib/bridge/server'
import { BaseBridge } from '~/lib/bridge/base'
import type { IRawBridge } from '~/lib/bridge/definitions'
/**
* A generic bridge class for Webivew2 or CefSharp.
*/
export class GenericBridge extends BaseBridge {
private bridge: IRawBridge
private archicadBridge: ArchicadBridge | undefined
private requests = {} as Record<
string,
{
methodName: string
resolve: (value: unknown) => void
reject: (reason: string | Error) => void
rejectTimerId: number
}
>
// TOTHINK: as this is a fast timeout, it forces us for long await methods in .net to return results via events. Kind-of not cool, and i'd be in favour of bumping it to "endless", or remove it altogether
// An example is the send or receive operations: they can take fucking long :D
private TIMEOUT_MS = 1000 * 60 // 60 sec
constructor(object: IRawBridge, isArchicadBridge: boolean = false) {
super()
this.bridge = object
if (isArchicadBridge) {
this.archicadBridge = new ArchicadBridge(this.emitter)
}
}
public async create(): Promise<boolean> {
// NOTE: GetMethods is a call to the .NET side.
try {
this.availableMethodNames = await this.bridge.GetBindingsMethodNames()
} catch {
console.warn(`Failed to get method names from binding.`)
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) {}
for (const methodName of this.availableMethodNames) {
const lowercasedMethodName = lowercaseMethodName(methodName)
const hoistTarget = this as unknown as Record<string, object>
hoistTarget[lowercasedMethodName] = (...args: unknown[]) =>
this.runMethod(methodName, args)
}
return true
}
private async emitResponseReady(eventName: string, requestId: string) {
this.registerPromise(eventName, requestId)
const data = await this.bridge.GetCallResult(requestId)
const request = this.requests[requestId]
try {
const parsedData = data ? (JSON.parse(data) as Record<string, unknown>) : null
if (parsedData === null) {
throw new Error(`Data is not parsed successfuly on ${eventName}`)
}
if (this.archicadBridge) {
this.archicadBridge.emit(eventName, parsedData, this.runMethod.bind(this))
} else {
this.emitter.emit(eventName, parsedData)
}
request.resolve(parsedData)
} catch (e) {
console.error(e)
request.reject(e as Error)
} finally {
window.clearTimeout(request.rejectTimerId)
delete this.requests[requestId]
}
}
async runMethod(
methodName: string,
args: unknown[],
shouldTimeout: boolean = true
): Promise<unknown> {
const requestId = (Math.random() + 1).toString(36).substring(2) + '_' + methodName
const preserializedArgs = args.map((a) => JSON.stringify(a))
this.bridge.RunMethod(methodName, requestId, JSON.stringify(preserializedArgs))
return this.registerPromise(methodName, requestId, shouldTimeout)
}
private async registerPromise(
methodName: string,
requestId: string,
shouldTimeout: boolean = true
) {
return new Promise((resolve, reject) => {
this.requests[requestId] = {
methodName,
resolve,
reject,
rejectTimerId: window.setTimeout(
() => {
reject(
`.NET response timed out for call to ${methodName} - did not receive anything back in good time (${this.TIMEOUT_MS}ms).`
)
delete this.requests[requestId]
},
shouldTimeout ? this.TIMEOUT_MS : 3600000
)
}
})
}
private async responseReady(requestId: string) {
if (!this.requests[requestId])
throw new Error(
`.NET Bridge found no request to resolve with the id of ${requestId}. Something is weird!`
)
const request = this.requests[requestId]
const data = await this.bridge.GetCallResult(requestId)
try {
const parsedData = data ? (JSON.parse(data) as Record<string, unknown>) : null // TODO: check if data is undefined
// eslint-disable-next-line no-prototype-builtins
if (parsedData && parsedData.hasOwnProperty('error')) {
console.error(data)
this.emitter.emit('errorOnResponse', data)
throw new Error(
`Failed to run ${requestId}. The host app error is logged above.`
)
}
request.resolve(parsedData)
} catch (e) {
console.error(e)
request.reject(e as Error)
} finally {
window.clearTimeout(request.rejectTimerId)
delete this.requests[requestId]
}
}
public showDevTools() {
this.bridge.ShowDevTools()
}
public openUrl(url: string) {
this.bridge.OpenUrl(url)
}
}
const lowercaseMethodName = (name: string) =>
name.charAt(0).toLowerCase() + name.slice(1)
+105 -19
View File
@@ -1,30 +1,47 @@
import { ArchicadBridge } from '~/lib/bridge/server'
import { BaseBridge } from '~/lib/bridge/base'
import type { IRawBridge } from '~/lib/bridge/definitions'
/**
* A generic bridge class for Webivew2 or CefSharp.
*/
export class GenericBridge extends BaseBridge {
private bridge: IRawBridge
private archicadBridge: ArchicadBridge | undefined
private requests = {} as Record<
string,
{
methodName: string
resolve: (value: unknown) => void
reject: (reason: string | Error) => void
rejectTimerId: number
}
>
// TOTHINK: as this is a fast timeout, it forces us for long await methods in .net to return results via events. Kind-of not cool, and i'd be in favour of bumping it to "endless", or remove it altogether
// An example is the send or receive operations: they can take fucking long :D
private TIMEOUT_MS = 1000 * 60 // 60 sec
constructor(object: IRawBridge) {
constructor(object: IRawBridge, isArchicadBridge: boolean = false) {
super()
this.bridge = object
if (isArchicadBridge) {
this.archicadBridge = new ArchicadBridge(this.emitter)
}
}
public async create(): Promise<boolean> {
// NOTE: GetMethods is a call to the .NET side.
let availableMethodNames = [] as string[]
try {
availableMethodNames = await this.bridge.GetBindingsMethodNames()
this.availableMethodNames = await this.bridge.GetBindingsMethodNames()
} catch {
console.warn(`Failed to get method names.`)
console.warn(`Failed to get method names from binding.`)
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) {}
for (const methodName of availableMethodNames) {
for (const methodName of this.availableMethodNames) {
const lowercasedMethodName = lowercaseMethodName(methodName)
const hoistTarget = this as unknown as Record<string, object>
hoistTarget[lowercasedMethodName] = (...args: unknown[]) =>
@@ -34,27 +51,96 @@ export class GenericBridge extends BaseBridge {
return true
}
private async runMethod(methodName: string, args: unknown[]): Promise<unknown> {
private async emitResponseReady(eventName: string, requestId: string) {
this.registerPromise(eventName, requestId)
const data = await this.bridge.GetCallResult(requestId)
const request = this.requests[requestId]
try {
const parsedData = data ? (JSON.parse(data) as Record<string, unknown>) : null
if (parsedData === null) {
throw new Error(`Data is not parsed successfuly on ${eventName}`)
}
if (this.archicadBridge) {
this.archicadBridge.emit(eventName, parsedData, this.runMethod.bind(this))
} else {
this.emitter.emit(eventName, parsedData)
}
request.resolve(parsedData)
} catch (e) {
console.error(e)
request.reject(e as Error)
} finally {
window.clearTimeout(request.rejectTimerId)
delete this.requests[requestId]
}
}
async runMethod(
methodName: string,
args: unknown[],
shouldTimeout: boolean = true
): Promise<unknown> {
const requestId = (Math.random() + 1).toString(36).substring(2) + '_' + methodName
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)
)
this.bridge.RunMethod(methodName, requestId, JSON.stringify(preserializedArgs))
const parsed = result ? (JSON.parse(result) as Record<string, unknown>) : null
return this.registerPromise(methodName, requestId, shouldTimeout)
}
if (parsed && parsed['error']) {
console.error(parsed)
private async registerPromise(
methodName: string,
requestId: string,
shouldTimeout: boolean = true
) {
return new Promise((resolve, reject) => {
this.requests[requestId] = {
methodName,
resolve,
reject,
rejectTimerId: window.setTimeout(
() => {
reject(
`.NET response timed out for call to ${methodName} - did not receive anything back in good time (${this.TIMEOUT_MS}ms).`
)
delete this.requests[requestId]
},
shouldTimeout ? this.TIMEOUT_MS : 3600000
)
}
})
}
private async responseReady(requestId: string) {
if (!this.requests[requestId])
throw new Error(
`Failed to run ${methodName} with args ${JSON.stringify(
args
)}. The host app error is logged above.`
`.NET Bridge found no request to resolve with the id of ${requestId}. Something is weird!`
)
}
return parsed
const request = this.requests[requestId]
const data = await this.bridge.GetCallResult(requestId)
try {
const parsedData = data ? (JSON.parse(data) as Record<string, unknown>) : null // TODO: check if data is undefined
// eslint-disable-next-line no-prototype-builtins
if (parsedData && parsedData.hasOwnProperty('error')) {
console.error(data)
this.emitter.emit('errorOnResponse', data)
throw new Error(
`Failed to run ${requestId}. The host app error is logged above.`
)
}
request.resolve(parsedData)
} catch (e) {
console.error(e)
request.reject(e as Error)
} finally {
window.clearTimeout(request.rejectTimerId)
delete this.requests[requestId]
}
}
public showDevTools() {
+4 -4
View File
@@ -189,7 +189,7 @@ export class ArchicadBridge {
const body: ArchicadReceiveRequest = {
accountId: eventPayload.accountId,
projectId: eventPayload.projectId,
referencedObject: result.data.project.model.version.referencedObject,
referencedObject: result.data.project.model.version.referencedObject as string,
xmlConverterPath: eventPayload.xmlConverterPath
}
@@ -272,7 +272,7 @@ export class ArchicadBridge {
serverUrl: account?.accountInfo.serverInfo.url as string,
token: account?.accountInfo.token as string,
streamId: eventPayload.projectId,
objectId: result.data.project.model.version.referencedObject
objectId: result.data.project.model.version.referencedObject as string
})
const updateProgress = (e: {
@@ -313,7 +313,7 @@ export class ArchicadBridge {
})
// CONVERSION WILL START AFTER THAT
await runMethod('afterGetObjects', args as unknown as unknown[])
await runMethod('afterGetObjects', args as unknown as unknown[], false)
}
private queuedPromises = {} as Record<string, Promise<Response>[]>
@@ -373,7 +373,7 @@ export class ArchicadBridge {
this.queuedPromises[modelCardId] = []
console.log(`🚀 Upload is completed in ${(performance.now() - start) / 1000} s!`)
const args = [eventPayload.modelCardId, referencedObjectId]
await runMethod('afterSendObjects', args as unknown as unknown[])
await runMethod('afterSendObjects', args as unknown as unknown[], false)
}
}
+2 -2
View File
@@ -86,7 +86,7 @@ export class SketchupBridge extends BaseBridge {
}
// 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 {
override emit(eventName: string, payload: string): void {
const eventPayload = payload as unknown as Record<string, unknown>
if (eventName === 'sendViaBrowser')
@@ -124,7 +124,7 @@ export class SketchupBridge extends BaseBridge {
serverUrl: account?.accountInfo.serverInfo.url as string,
token: account?.accountInfo.token as string,
streamId: eventPayload.projectId,
objectId: result.data.project.model.version.referencedObject
objectId: result.data.project.model.version.referencedObject as string
})
const updateProgress = (e: {
+3 -3
View File
@@ -1,7 +1,7 @@
import crs from 'crypto-random-string'
import type { AutomationRunItemFragment } from 'lib/common/generated/gql/graphql'
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { CardSetting } from 'lib/models/card/setting'
import type { AutomationRunItemFragment } from '~/lib/common/generated/gql/graphql'
import type { ConversionResult } from '~/lib/conversions/conversionResult'
import type { CardSetting } from '~/lib/models/card/setting'
import type { IDiscriminatedObject } from '~~/lib/bindings/definitions/common'
import { DiscriminatedObject } from '~~/lib/bindings/definitions/common'
+4 -2
View File
@@ -1,9 +1,11 @@
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { ConversionResult } from '~/lib/conversions/conversionResult'
export type ModelCardNotificationLevel = 'info' | 'danger' | 'warning' | 'success'
export type ModelCardNotification = {
modelCardId: string
text: string
level: 'info' | 'danger' | 'warning' | 'success'
level: ModelCardNotificationLevel
cta?: {
name: string
action: () => void
+1
View File
@@ -53,6 +53,7 @@ export default defineNuxtConfig({
]
},
ssr: false,
devtools: { enabled: false },
build: {
transpile: [
/^@apollo\/client/,
+9 -8
View File
@@ -15,7 +15,7 @@
"postinstall": "nuxt prepare",
"lint:js": "eslint .",
"lint:tsc": "vue-tsc --noEmit",
"lint:prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --check .",
"lint:prettier": "prettier --config .prettierrc --ignore-path .prettierignore --check .",
"lint:css": "stylelint \"**/*.{css,vue}\"",
"lint": "yarn lint:js && yarn lint:tsc && yarn lint:prettier && yarn lint:css",
"lint:ci": "yarn lint:tsc && yarn lint:css",
@@ -55,11 +55,11 @@
"devDependencies": {
"@graphql-codegen/cli": "^5.0.5",
"@graphql-codegen/client-preset": "^4.3.0",
"@nuxt/eslint": "^0.3.13",
"@nuxt/eslint": "^1.3.1",
"@nuxtjs/tailwindcss": "^6.14.0",
"@parcel/watcher": "^2.5.1",
"@types/apollo-upload-client": "^17.0.1",
"@types/eslint": "^8.56.10",
"@types/eslint": "^9.6.1",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^8.20.0",
@@ -77,10 +77,9 @@
"postcss-html": "^1.8.0",
"postcss-nesting": "^13.0.1",
"prettier": "^2.8.7",
"stylelint": "^15.10.1",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^26.0.0",
"stylelint": "^16.19.1",
"stylelint-config-recommended-vue": "^1.6.0",
"stylelint-config-standard": "^38.0.0",
"tailwindcss": "^3.4.17",
"type-fest": "^3.5.1",
"typescript": "^5.7.3",
@@ -89,7 +88,9 @@
},
"resolutions": {
"core-js": "3.22.4",
"core-js-compat/semver": "^7.5.4"
"core-js-compat/semver": "^7.5.4",
"@babel/plugin-transform-classes/globals": "13.13.0",
"@babel/traverse/globals": "13.13.0"
},
"packageManager": "yarn@4.9.1"
}
+1 -1
View File
@@ -79,7 +79,7 @@
</p>
</div>
<FormButton
color="card"
:color="'outline'"
full-width
class="sticky top-10 top-16"
@click="runTests()"
+1 -1
View File
@@ -1,5 +1,5 @@
import type { IRawBridge } from '~/lib/bridge/definitions'
import { GenericBridge } from '~/lib/bridge/generic-v2'
import { GenericBridge } from '~/lib/bridge/generic'
import { SketchupBridge } from '~/lib/bridge/sketchup'
import type { IBasicConnectorBinding } from '~/lib/bindings/definitions/IBasicConnectorBinding'
+2 -3
View File
@@ -15,7 +15,6 @@ import { onError, type ErrorResponse } from '@apollo/client/link/error'
import { getMainDefinition } from '@apollo/client/utilities'
import { setContext } from '@apollo/client/link/context'
import { useHostAppStore } from '~/store/hostApp'
import type { ToastNotification } from '@speckle/ui-components'
import { ToastNotificationType } from '@speckle/ui-components'
export type DUIAccount = {
@@ -86,7 +85,7 @@ export const useAccountStore = defineStore('accountStore', () => {
try {
await acc.client.query({ query: accountTestQuery })
acc.isValid = true
} catch (error) {
} catch {
// TODO: properly dispose and kill this client. It's unclear how to do it.
acc.isValid = false
// NOTE: we do not want to delete the client, as we might want to "refresh" in
@@ -116,7 +115,7 @@ export const useAccountStore = defineStore('accountStore', () => {
if (res.graphQLErrors) {
if (
res.graphQLErrors?.some(
(err) => err.extensions.code === 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR'
(err) => err.extensions?.code === 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR'
)
) {
hostAppStore.setNotification({
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ConnectorConfig } from 'lib/bindings/definitions/IConfigBinding'
import type { ConnectorConfig } from '~/lib/bindings/definitions/IConfigBinding'
import { defineStore } from 'pinia'
export const useConfigStore = defineStore('configStore', () => {
+7 -4
View File
@@ -2,7 +2,7 @@ import type {
DocumentInfo,
DocumentModelStore
} from '~/lib/bindings/definitions/IBasicConnectorBinding'
import type { IModelCard, ModelCardProgress } from 'lib/models/card'
import type { IModelCard, ModelCardProgress } from '~/lib/models/card'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import type { IReceiverModelCard } from '~/lib/models/card/receiver'
import type {
@@ -12,11 +12,11 @@ import type {
ISenderModelCard,
RevitViewsSendFilter,
SendFilterSelect
} from 'lib/models/card/send'
} from '~/lib/models/card/send'
import type { ToastNotification } from '@speckle/ui-components'
import type { Nullable } from '@speckle/shared'
import type { HostAppError } from '~/lib/bridge/errorHandler'
import type { ConversionResult } from 'lib/conversions/conversionResult'
import type { ConversionResult } from '~/lib/conversions/conversionResult'
import { defineStore } from 'pinia'
import type { CardSetting } from '~/lib/models/card/setting'
import type { DUIAccount } from '~/store/accounts'
@@ -489,7 +489,10 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
if (typeof args.error === 'string') {
model.error = { errorMessage: args.error as string, dismissible: true }
} else {
model.error = args.error as { errorMessage: string; dismissible: boolean }
model.error = args.error as {
errorMessage: string
dismissible: boolean
}
}
}
+2 -6
View File
@@ -1,9 +1,5 @@
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-recommended-vue',
'stylelint-config-prettier'
],
extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
// add your custom config here
// https://stylelint.io/user-guide/configuration
rules: {
@@ -14,7 +10,7 @@ module.exports = {
ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen']
}
],
'declaration-block-trailing-semicolon': null,
'at-rule-no-deprecated': [true, { ignoreAtRules: ['apply'] }],
'no-descending-specificity': null
},
overrides: [
+1 -1
View File
@@ -1,4 +1,4 @@
import {plugin as speckleThemePlugin} from '@speckle/tailwind-theme'
import { plugin as speckleThemePlugin } from '@speckle/tailwind-theme'
import { tailwindContentEntries as themeEntries } from '@speckle/tailwind-theme/tailwind-configure'
import { tailwindContentEntries as uiLibEntries } from '@speckle/ui-components/tailwind-configure'
import formsPlugin from '@tailwindcss/forms'
+759 -1168
View File
File diff suppressed because it is too large Load Diff