chore: finish removing dui3 (#4738)
This commit is contained in:
committed by
GitHub
parent
380e55b7aa
commit
a1f8f79b7a
@@ -13,10 +13,6 @@ services:
|
|||||||
source: node_modules # top-level volume
|
source: node_modules # top-level volume
|
||||||
target: /workspaces/speckle-server/node_modules
|
target: /workspaces/speckle-server/node_modules
|
||||||
read_only: false
|
read_only: false
|
||||||
- type: volume
|
|
||||||
source: dui3-node_modules
|
|
||||||
target: /workspaces/speckle-server/packages/dui3/node_modules
|
|
||||||
read_only: false
|
|
||||||
- type: volume
|
- type: volume
|
||||||
source: fileimport-service-node_modules
|
source: fileimport-service-node_modules
|
||||||
target: /workspaces/speckle-server/packages/fileimport-service/node_modules
|
target: /workspaces/speckle-server/packages/fileimport-service/node_modules
|
||||||
@@ -104,10 +100,6 @@ services:
|
|||||||
source: node_modules
|
source: node_modules
|
||||||
target: /workspaces/speckle-server/node_modules
|
target: /workspaces/speckle-server/node_modules
|
||||||
read_only: false
|
read_only: false
|
||||||
- type: volume
|
|
||||||
source: dui3-node_modules
|
|
||||||
target: /workspaces/speckle-server/packages/dui3/node_modules
|
|
||||||
read_only: false
|
|
||||||
- type: volume
|
- type: volume
|
||||||
source: fileimport-service-node_modules
|
source: fileimport-service-node_modules
|
||||||
target: /workspaces/speckle-server/packages/fileimport-service/node_modules
|
target: /workspaces/speckle-server/packages/fileimport-service/node_modules
|
||||||
@@ -176,7 +168,6 @@ volumes:
|
|||||||
# (this allows the devcontainer to be based on linux yet work on Apple Silicon etc..)
|
# (this allows the devcontainer to be based on linux yet work on Apple Silicon etc..)
|
||||||
# If you add a new package with a new `node_modules`, it needs to be added here
|
# If you add a new package with a new `node_modules`, it needs to be added here
|
||||||
node_modules:
|
node_modules:
|
||||||
dui3-node_modules:
|
|
||||||
fileimport-service-node_modules:
|
fileimport-service-node_modules:
|
||||||
frontend-2-node_modules:
|
frontend-2-node_modules:
|
||||||
monitor-deployment-node_modules:
|
monitor-deployment-node_modules:
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@
|
|||||||
"dev": "yarn workspaces foreach -pivW -j unlimited run dev",
|
"dev": "yarn workspaces foreach -pivW -j unlimited run dev",
|
||||||
"dev:no-server": "yarn workspaces foreach --exclude @speckle/server -pivW -j unlimited run dev",
|
"dev:no-server": "yarn workspaces foreach --exclude @speckle/server -pivW -j unlimited run dev",
|
||||||
"dev:minimal": "yarn workspaces foreach -pivW -j unlimited --include '{@speckle/server,@speckle/frontend-2}' run dev",
|
"dev:minimal": "yarn workspaces foreach -pivW -j unlimited --include '{@speckle/server,@speckle/frontend-2}' run dev",
|
||||||
"gqlgen": "yarn workspaces foreach -pivW -j unlimited --include '{@speckle/server,@speckle/frontend,@speckle/frontend-2,@speckle/dui3}' run gqlgen",
|
"gqlgen": "yarn workspaces foreach -pivW -j unlimited --include '{@speckle/server,@speckle/frontend,@speckle/frontend-2}' run gqlgen",
|
||||||
"dev:server": "yarn workspace @speckle/server dev",
|
"dev:server": "yarn workspace @speckle/server dev",
|
||||||
"dev:frontend-2": "yarn workspace @speckle/frontend-2 dev",
|
"dev:frontend-2": "yarn workspace @speckle/frontend-2 dev",
|
||||||
"dev:shared": "yarn workspace @speckle/shared dev",
|
"dev:shared": "yarn workspace @speckle/shared dev",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
HOST=0.0.0.0
|
|
||||||
PORT=8082
|
|
||||||
|
|
||||||
NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
|
|
||||||
NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
|
|
||||||
|
|
||||||
##########################################################
|
|
||||||
# Local dev settings
|
|
||||||
##########################################################
|
|
||||||
# Uncomment to enable pino-pretty log formatting in debug mode (disabled cause of node22 issues)
|
|
||||||
# ALLOW_PRETTY_DEBUGGER=true
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
node_modules
|
|
||||||
*.log*
|
|
||||||
.nuxt
|
|
||||||
.nitro
|
|
||||||
.cache
|
|
||||||
.output
|
|
||||||
.env
|
|
||||||
dist
|
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
Vendored
-13
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"css.validate": false,
|
|
||||||
"less.validate": false,
|
|
||||||
"scss.validate": false,
|
|
||||||
"stylelint.validate": ["css", "scss", "vue", "postcss"],
|
|
||||||
"stylelint.enable": true,
|
|
||||||
"stylelint.configFile": "${workspaceFolder}/stylelint.config.js",
|
|
||||||
"volar.completion.preferredTagNameCase": "pascal",
|
|
||||||
"javascript.suggest.autoImports": true,
|
|
||||||
"typescript.suggest.autoImports": true,
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
|
||||||
"javascript.preferences.importModuleSpecifier": "non-relative"
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# dui3
|
|
||||||
|
|
||||||
DUIv3 is a Speckle interface embedded inside the desktop connectors that allows users to interact with them - sync streams, manage servers etc. It's built in Vue 3 with Nuxt 3 and only supports client side rendering.
|
|
||||||
|
|
||||||
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
Make sure to install the dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# yarn
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
And create an `.env` file from `.env.example`.
|
|
||||||
|
|
||||||
## Development Server
|
|
||||||
|
|
||||||
Start the development server on `http://localhost:3000`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production
|
|
||||||
|
|
||||||
Build the application for production:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Locally preview production build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="speckle" class="bg-foundation-page text-foreground">
|
|
||||||
<NuxtLayout>
|
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useAccountsSetup } from '~/lib/accounts/composables/setup'
|
|
||||||
import { useDocumentInfoStore } from '~/store/uiConfig'
|
|
||||||
|
|
||||||
const uiConfigStore = useDocumentInfoStore()
|
|
||||||
const { isDarkTheme } = storeToRefs(uiConfigStore)
|
|
||||||
|
|
||||||
useAccountsSetup()
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
// Title suffix
|
|
||||||
titleTemplate: (titleChunk) =>
|
|
||||||
titleChunk ? `${titleChunk as string} - Speckle DUIv3` : 'Speckle DUIv3',
|
|
||||||
htmlAttrs: {
|
|
||||||
lang: 'en',
|
|
||||||
class: computed(() => (isDarkTheme.value ? `dark` : ``))
|
|
||||||
},
|
|
||||||
bodyAttrs: {
|
|
||||||
class: 'simple-scrollbar bg-foundation-page text-foreground'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/* stylelint-disable selector-id-pattern */
|
|
||||||
@import '@speckle/ui-components/style.css';
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Don't pollute this - it's going to be bundled in all pages!
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Making sure page is always stretched to the bottom of the screen even if there's nothing in it
|
|
||||||
*/
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
div#__nuxt,
|
|
||||||
div#__nuxt > div {
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
div#__nuxt {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,28 +0,0 @@
|
|||||||
import type { CodegenConfig } from '@graphql-codegen/cli'
|
|
||||||
|
|
||||||
const config: CodegenConfig = {
|
|
||||||
schema: 'http://127.0.0.1:3000/graphql',
|
|
||||||
documents: ['{lib,components,layouts,pages,middleware}/**/*.{vue,js,ts}'],
|
|
||||||
ignoreNoDocuments: true, // for better experience with the watcher
|
|
||||||
generates: {
|
|
||||||
'./lib/common/generated/gql/': {
|
|
||||||
preset: 'client',
|
|
||||||
config: {
|
|
||||||
useTypeImports: true,
|
|
||||||
fragmentMasking: false,
|
|
||||||
dedupeFragments: true,
|
|
||||||
scalars: {
|
|
||||||
JSONObject: '{}',
|
|
||||||
DateTime: 'string'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
presetConfig: {
|
|
||||||
fragmentMasking: false,
|
|
||||||
dedupeFragments: true
|
|
||||||
},
|
|
||||||
plugins: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<template>
|
|
||||||
<NuxtLink class="flex items-center" to="/">
|
|
||||||
<img
|
|
||||||
class="block h-6 w-6"
|
|
||||||
:class="{ 'mr-2': !minimal, grayscale: active }"
|
|
||||||
src="~~/assets/images/speckle_logo_big.png"
|
|
||||||
alt="Speckle"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="!minimal"
|
|
||||||
class="text-primary h6 mt-0 hidden font-bold leading-7 md:flex"
|
|
||||||
>
|
|
||||||
Speckle
|
|
||||||
</div>
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps({
|
|
||||||
minimal: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<nav
|
|
||||||
class="fixed top-0 h-14 bg-foundation max-w-full w-full shadow hover:shadow-md transition z-20"
|
|
||||||
>
|
|
||||||
<div class="px-2">
|
|
||||||
<div class="flex items-center h-14 transition-all justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<HeaderLogoBlock :active="false" class="mr-0" />
|
|
||||||
<div class="flex flex-shrink-0 items-center -ml-2 md:ml-0">
|
|
||||||
<PortalTarget name="navigation"></PortalTarget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<HeaderUserMenu />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="transition text-foreground hover:text-primary-focus">
|
|
||||||
<NuxtLink
|
|
||||||
:to="to"
|
|
||||||
class="flex items-center text-sm"
|
|
||||||
active-class="text-primary font-bold"
|
|
||||||
>
|
|
||||||
<div v-if="separator">
|
|
||||||
<ChevronRightIcon class="flex w-4 h-4 mt-[3px] mx-0 md:mx-1" />
|
|
||||||
</div>
|
|
||||||
<div class="max-w-[120px] md:max-w-[200px] lg:max-w-[300px] truncate">
|
|
||||||
{{ name || to }}
|
|
||||||
</div>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ChevronRightIcon } from '@heroicons/vue/20/solid'
|
|
||||||
defineProps({
|
|
||||||
separator: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
type: String,
|
|
||||||
default: '/'
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button
|
|
||||||
v-tippy="tip"
|
|
||||||
:class="`block w-full text-left items-center space-x-2 hover:bg-primary-muted transition p-2 select-none group hover:cursor-pointer hover:text-primary ${
|
|
||||||
!account.isValid ? 'text-danger bg-rose-500/10' : ''
|
|
||||||
} ${account.accountInfo.isDefault ? 'bg-blue-500/5' : ''}`"
|
|
||||||
@click="$openUrl(account.accountInfo.serverInfo.url)"
|
|
||||||
>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<UserAvatar :user="userAvatar" :active="account.accountInfo.isDefault" />
|
|
||||||
<div class="min-w-0 grow">
|
|
||||||
<div class="truncate overflow-hidden min-w-0">
|
|
||||||
{{ account.accountInfo.serverInfo.name }}
|
|
||||||
<span class="text-foreground-2 truncate min-w-0">
|
|
||||||
{{ account.accountInfo.serverInfo.url.split('//')[1] }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="truncate text-xs text-foreground-2">
|
|
||||||
{{ account.accountInfo.userInfo.email }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="transition opacity-0 group-hover:opacity-100">
|
|
||||||
<ArrowTopRightOnSquareIcon class="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { DUIAccount } from 'lib/accounts/composables/setup'
|
|
||||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/20/solid'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
account: DUIAccount
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const userAvatar = computed(() => {
|
|
||||||
return {
|
|
||||||
name: props.account.accountInfo.userInfo.name,
|
|
||||||
avatar: props.account.accountInfo.userInfo.avatar
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const tip = computed(() => {
|
|
||||||
let value = ''
|
|
||||||
if (props.account.accountInfo.isDefault) value += 'This is your default account. '
|
|
||||||
if (!props.account.isValid) value += 'This account is not reachable.'
|
|
||||||
return value === '' ? null : value
|
|
||||||
})
|
|
||||||
|
|
||||||
const { $openUrl } = useNuxtApp()
|
|
||||||
</script>
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Menu as="div" class="ml-2 flex items-center">
|
|
||||||
<MenuButton v-slot="{ open }">
|
|
||||||
<span class="sr-only">Open user menu</span>
|
|
||||||
|
|
||||||
<UserAvatar v-if="!open" size="lg" :user="user" hover-effect />
|
|
||||||
<UserAvatar v-else size="lg" hover-effect>
|
|
||||||
<XMarkIcon class="w-5 h-5" />
|
|
||||||
</UserAvatar>
|
|
||||||
</MenuButton>
|
|
||||||
<Transition
|
|
||||||
enter-active-class="transition ease-out duration-200"
|
|
||||||
enter-from-class="transform opacity-0 scale-95"
|
|
||||||
enter-to-class="transform opacity-100 scale-100"
|
|
||||||
leave-active-class="transition ease-in duration-75"
|
|
||||||
leave-from-class="transform opacity-100 scale-100"
|
|
||||||
leave-to-class="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<MenuItems
|
|
||||||
class="absolute right-0 md:right-4 top-14 md:top-16 w-full md:w-64 origin-top-right bg-foundation sm:rounded-t-md rounded-b-md shadow-lg overflow-hidden"
|
|
||||||
>
|
|
||||||
<MenuItem>
|
|
||||||
<div class="border border-t-1 border-primary-muted">
|
|
||||||
<div v-if="loading" class="p-2">Loading accounts...</div>
|
|
||||||
<div v-else class="p-2 flex items-center justify-between">
|
|
||||||
<div class="text-xs text-foreground-2">Your accounts</div>
|
|
||||||
<div>
|
|
||||||
<FormButton
|
|
||||||
text
|
|
||||||
size="xs"
|
|
||||||
@click.stop="accountStore.refreshAccounts()"
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</FormButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-0">
|
|
||||||
<HeaderUserAccount
|
|
||||||
v-for="acc in accounts"
|
|
||||||
:key="acc.accountInfo.id"
|
|
||||||
:account="(acc as DUIAccount)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem v-slot="{ close }" as="div">
|
|
||||||
<div class="px-2 py-3 flex space-x-2 border-t-1 justify-between">
|
|
||||||
<div class="">
|
|
||||||
<button
|
|
||||||
class="text-xs text-foreground-2 hover:text-primary transition"
|
|
||||||
@click="$showDevTools"
|
|
||||||
>
|
|
||||||
Open Dev Tools
|
|
||||||
</button>
|
|
||||||
<NuxtLink
|
|
||||||
class="text-xs text-foreground-2 hover:text-primary transition"
|
|
||||||
to="/test"
|
|
||||||
@click="close()"
|
|
||||||
>
|
|
||||||
Test Page
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
<!--
|
|
||||||
NOTE: Here's an example of customising the frontend app based on what bindings we
|
|
||||||
have loaded. E.g., if config bindings are not present, we do not show any button
|
|
||||||
regarding switching themes.
|
|
||||||
-->
|
|
||||||
<div v-if="hasConfigBindings">
|
|
||||||
<FormButton size="xs" text @click.stop="toggleTheme()">
|
|
||||||
{{ isDarkTheme ? 'Switch To Light Theme' : 'Switch To Dark Theme' }}
|
|
||||||
</FormButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
</MenuItems>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { XMarkIcon } from '@heroicons/vue/20/solid'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
|
||||||
import { useAccountStore } from '~/store/accounts'
|
|
||||||
import type { DUIAccount } from '~/lib/accounts/composables/setup'
|
|
||||||
import { useDocumentInfoStore } from '~/store/uiConfig'
|
|
||||||
|
|
||||||
const accountStore = useAccountStore()
|
|
||||||
const { accounts, defaultAccount, loading } = storeToRefs(accountStore)
|
|
||||||
|
|
||||||
const uiConfigStore = useDocumentInfoStore()
|
|
||||||
const { isDarkTheme, hasConfigBindings } = storeToRefs(uiConfigStore)
|
|
||||||
const { toggleTheme } = uiConfigStore
|
|
||||||
|
|
||||||
const { $showDevTools } = useNuxtApp()
|
|
||||||
|
|
||||||
const user = computed(() => {
|
|
||||||
if (!defaultAccount.value) return undefined
|
|
||||||
return {
|
|
||||||
name: defaultAccount.value?.accountInfo.userInfo.name,
|
|
||||||
avatar: defaultAccount.value?.accountInfo.userInfo.avatar
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'text-foreground-on-primary flex shrink-0 items-center justify-center overflow-hidden rounded-full font-semibold uppercase transition',
|
|
||||||
sizeClasses,
|
|
||||||
bgClasses,
|
|
||||||
borderClasses,
|
|
||||||
hoverClasses,
|
|
||||||
activeClasses
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<slot>
|
|
||||||
<div
|
|
||||||
v-if="user?.avatar"
|
|
||||||
class="h-full w-full bg-cover bg-center bg-no-repeat"
|
|
||||||
:style="{ backgroundImage: `url('${user.avatar}')` }"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else-if="initials"
|
|
||||||
class="flex h-full w-full select-none items-center justify-center"
|
|
||||||
>
|
|
||||||
{{ initials }}
|
|
||||||
</div>
|
|
||||||
<div v-else><UserCircleIcon :class="iconClasses" /></div>
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { UserCircleIcon } from '@heroicons/vue/20/solid'
|
|
||||||
type UserAvatar = {
|
|
||||||
name: string
|
|
||||||
avatar?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UserAvatarSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | 'editable'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
user?: UserAvatar
|
|
||||||
size?: UserAvatarSize
|
|
||||||
hoverEffect?: boolean
|
|
||||||
active?: boolean
|
|
||||||
noBorder?: boolean
|
|
||||||
noBackground?: boolean
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
user: undefined,
|
|
||||||
size: 'base',
|
|
||||||
hoverEffect: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const initials = computed(() => {
|
|
||||||
if (!props.user?.name.length) return
|
|
||||||
const parts = props.user.name.split(' ')
|
|
||||||
const firstLetter = parts[0]?.[0] || ''
|
|
||||||
const secondLetter = parts[1]?.[0] || ''
|
|
||||||
|
|
||||||
if (props.size === 'sm' || props.size === 'xs') return firstLetter
|
|
||||||
return firstLetter + secondLetter
|
|
||||||
})
|
|
||||||
|
|
||||||
const borderClasses = computed(() => {
|
|
||||||
if (props.noBorder) return ''
|
|
||||||
return 'border-2 border-foundation'
|
|
||||||
})
|
|
||||||
|
|
||||||
const bgClasses = computed(() => {
|
|
||||||
if (props.noBackground) return ''
|
|
||||||
return 'bg-primary'
|
|
||||||
})
|
|
||||||
|
|
||||||
const hoverClasses = computed(() => {
|
|
||||||
if (props.hoverEffect)
|
|
||||||
return 'hover:border-primary focus:border-primary active:scale-95'
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const activeClasses = computed(() => {
|
|
||||||
if (props.active) return 'border-primary'
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const heightClasses = computed(() => {
|
|
||||||
const size = props.size
|
|
||||||
switch (size) {
|
|
||||||
case 'xs':
|
|
||||||
return 'h-5'
|
|
||||||
case 'sm':
|
|
||||||
return 'h-6'
|
|
||||||
case 'lg':
|
|
||||||
return 'h-10'
|
|
||||||
case 'xl':
|
|
||||||
return 'h-14'
|
|
||||||
case 'editable':
|
|
||||||
return 'h-60'
|
|
||||||
case 'base':
|
|
||||||
default:
|
|
||||||
return 'h-8'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const widthClasses = computed(() => {
|
|
||||||
const size = props.size
|
|
||||||
switch (size) {
|
|
||||||
case 'xs':
|
|
||||||
return 'w-5'
|
|
||||||
case 'sm':
|
|
||||||
return 'w-6'
|
|
||||||
case 'lg':
|
|
||||||
return 'w-10'
|
|
||||||
case 'xl':
|
|
||||||
return 'w-14'
|
|
||||||
case 'editable':
|
|
||||||
return 'w-60'
|
|
||||||
case 'base':
|
|
||||||
default:
|
|
||||||
return 'w-8'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const textClasses = computed(() => {
|
|
||||||
const size = props.size
|
|
||||||
switch (size) {
|
|
||||||
case 'xs':
|
|
||||||
return 'text-tiny'
|
|
||||||
case 'sm':
|
|
||||||
return 'text-xs'
|
|
||||||
case 'lg':
|
|
||||||
return 'text-md'
|
|
||||||
case 'xl':
|
|
||||||
return 'text-2xl'
|
|
||||||
case 'editable':
|
|
||||||
return 'h1'
|
|
||||||
case 'base':
|
|
||||||
default:
|
|
||||||
return 'text-sm'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const iconClasses = computed(() => {
|
|
||||||
const size = props.size
|
|
||||||
switch (size) {
|
|
||||||
case 'xs':
|
|
||||||
return 'w-3 h-3'
|
|
||||||
case 'sm':
|
|
||||||
return 'w-3 h-3'
|
|
||||||
case 'lg':
|
|
||||||
return 'w-5 h-5'
|
|
||||||
case 'xl':
|
|
||||||
return 'w-8 h-8'
|
|
||||||
case 'editable':
|
|
||||||
return 'w-20 h-20'
|
|
||||||
case 'base':
|
|
||||||
default:
|
|
||||||
return 'w-4 h-4'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const sizeClasses = computed(
|
|
||||||
() => `${widthClasses.value} ${heightClasses.value} ${textClasses.value}`
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
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'
|
|
||||||
|
|
||||||
const configs = await withNuxt([
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
camelcase: [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
properties: 'always',
|
|
||||||
allow: ['^[\\w]+_[\\w]+Fragment$']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'no-alert': 'error',
|
|
||||||
eqeqeq: ['error', 'always', { null: 'always' }],
|
|
||||||
'no-console': 'off',
|
|
||||||
'no-var': 'error'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,vue,tsx,mts,cts}'],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.eslint.json'],
|
|
||||||
extraFileExtensions: ['.vue'],
|
|
||||||
tsconfigRootDir: getESMDirname(import.meta.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.test.{ts,js}'],
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
...globals.jest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['./{components|pages|store|lib}/*.{js,ts,vue}'],
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
...globals.browser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx,vue}'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-explicit-any': ['error'],
|
|
||||||
'@typescript-eslint/no-unsafe-argument': ['error'],
|
|
||||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
|
||||||
'@typescript-eslint/no-unsafe-call': 'error',
|
|
||||||
'@typescript-eslint/no-unsafe-member-access': 'error',
|
|
||||||
'@typescript-eslint/no-unsafe-return': 'error',
|
|
||||||
'@typescript-eslint/no-for-in-array': ['error'],
|
|
||||||
'@typescript-eslint/restrict-plus-operands': ['error'],
|
|
||||||
'@typescript-eslint/await-thenable': ['warn'],
|
|
||||||
'@typescript-eslint/no-restricted-types': ['warn'],
|
|
||||||
'require-await': 'off',
|
|
||||||
'@typescript-eslint/require-await': 'error',
|
|
||||||
'no-undef': 'off',
|
|
||||||
|
|
||||||
'@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
|
|
||||||
'@typescript-eslint/no-invalid-void-type': 'off' // too restrictive
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...pluginVueA11y.configs['flat/recommended'].map((c) => ({
|
|
||||||
...c,
|
|
||||||
files: [...(c.files || []), '**/*.vue'],
|
|
||||||
languageOptions: c.languageOptions
|
|
||||||
? omit(c.languageOptions, ['parserOptions', 'parser']) // Prevent overriding parser
|
|
||||||
: undefined
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
files: ['**/*.vue'],
|
|
||||||
rules: {
|
|
||||||
'vue/component-tags-order': [
|
|
||||||
'error',
|
|
||||||
{ order: ['docs', 'template', 'script', 'style'] }
|
|
||||||
],
|
|
||||||
'vue/require-default-prop': 'off',
|
|
||||||
'vue/multi-word-component-names': 'off',
|
|
||||||
'vue/component-name-in-template-casing': [
|
|
||||||
'error',
|
|
||||||
'PascalCase',
|
|
||||||
{ registeredComponentsOnly: false }
|
|
||||||
],
|
|
||||||
'vuejs-accessibility/label-has-for': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
required: {
|
|
||||||
some: ['nesting', 'id']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'vue/html-self-closing': 'off' // messes with prettier
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ['**/*.d.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-var': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-restricted-types': 'off'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]).prepend([
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
'**/node_modules/**',
|
|
||||||
'**/templates/*',
|
|
||||||
'./lib/common/generated/**/*',
|
|
||||||
'storybook-static',
|
|
||||||
'.nuxt/**',
|
|
||||||
'.output/**'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...baseConfigs
|
|
||||||
])
|
|
||||||
|
|
||||||
export default configs
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-full">
|
|
||||||
<HeaderNavBar />
|
|
||||||
<main class="px-1 pb-4 mt-16">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/require-await */
|
|
||||||
|
|
||||||
import { BaseBridge } from '~~/lib/bridge/base'
|
|
||||||
import type { IBinding } from '~~/lib/bindings/definitions/IBinding'
|
|
||||||
|
|
||||||
export const IBasicConnectorBindingKey = 'baseBinding'
|
|
||||||
|
|
||||||
// Needs to be agreed between Frontend and Core
|
|
||||||
export interface IBasicConnectorBinding
|
|
||||||
extends IBinding<IBasicConnectorBindingHostEvents> {
|
|
||||||
getAccounts: () => Promise<Account[]>
|
|
||||||
getSourceApplicationName: () => Promise<string>
|
|
||||||
getSourceApplicationVersion: () => Promise<string>
|
|
||||||
getDocumentInfo: () => Promise<DocumentInfo>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IBasicConnectorBindingHostEvents {
|
|
||||||
displayToastNotification: (args: ToastInfo) => void
|
|
||||||
documentChanged: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// An almost 1-1 mapping of what we need from the Core accounts class.
|
|
||||||
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 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 class MockedBaseBinding extends BaseBridge {
|
|
||||||
public async getAccounts() {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getSourceApplicationName() {
|
|
||||||
return 'Mocks'
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getSourceApplicationVersion() {
|
|
||||||
return Math.random().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDocumentInfo() {
|
|
||||||
return {
|
|
||||||
name: 'Mocked File',
|
|
||||||
location: 'www',
|
|
||||||
id: Math.random().toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async showDevTools() {
|
|
||||||
console.log('Mocked bindings cannot do this')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* Basic interface scaffolding two standard method.
|
|
||||||
*/
|
|
||||||
export interface IBinding<T> {
|
|
||||||
/**
|
|
||||||
* Events sent over from the host application.
|
|
||||||
*/
|
|
||||||
on: <E extends keyof T>(event: E, callback: T[E]) => void
|
|
||||||
/**
|
|
||||||
* If possible, opens up dev tools from the embedded browser window.
|
|
||||||
* Currently needed for CefSharp, as right click inspect doesn't exist.
|
|
||||||
*/
|
|
||||||
showDevTools: () => Promise<void>
|
|
||||||
/**
|
|
||||||
* Opens an url in the OS's default browser.
|
|
||||||
*/
|
|
||||||
openUrl: (url: string) => Promise<void>
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { BaseBridge } from '~~/lib/bridge/base'
|
|
||||||
import type { IBinding } from '~~/lib/bindings/definitions/IBinding'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name under which this binding will be registered.
|
|
||||||
*/
|
|
||||||
export const IConfigBindingKey = 'configBinding'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A test binding interface to ensure compatbility. Ideally all host environments would implement and register it.
|
|
||||||
*/
|
|
||||||
export interface IConfigBinding extends IBinding<IConfigBindingEvents> {
|
|
||||||
getConfig: () => Promise<Config>
|
|
||||||
updateConfig: (config: Config) => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IConfigBindingEvents {
|
|
||||||
void: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Config = {
|
|
||||||
darkTheme: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MockedConfigBinding extends BaseBridge {
|
|
||||||
getConfig() {
|
|
||||||
return {
|
|
||||||
darkTheme: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
updateConfig(config: Config) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/require-await */
|
|
||||||
|
|
||||||
import { BaseBridge } from '~~/lib/bridge/base'
|
|
||||||
import type { IBinding } from '~~/lib/bindings/definitions/IBinding'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 extends IBinding<ITestBindingEvents> {
|
|
||||||
sayHi: (name: string, count: number, sayHelloNotHi: boolean) => Promise<string[]>
|
|
||||||
goAway: () => Promise<void>
|
|
||||||
getComplexType: () => Promise<ComplexType>
|
|
||||||
shouldThrow: () => Promise<void>
|
|
||||||
triggerEvent: (eventName: string) => Promise<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
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MockedTestBinding extends BaseBridge {
|
|
||||||
public async sayHi(name: string, count: number, sayHelloNotHi: boolean) {
|
|
||||||
return `Hello from mocked bindings. Args: name = ${name}, count = ${count}, sayHelloNotHi = ${sayHelloNotHi.toString()}.`
|
|
||||||
}
|
|
||||||
|
|
||||||
public async goAway() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getComplexType() {
|
|
||||||
return { id: 'wow', count: 42 }
|
|
||||||
}
|
|
||||||
|
|
||||||
public async shouldThrow() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
public async triggerEvent(eventName: string) {
|
|
||||||
return eventName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { Emitter } from 'nanoevents'
|
|
||||||
import { createNanoEvents } 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 {
|
|
||||||
public 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: this could be private - as it should be only used by the host application.
|
|
||||||
emit(eventName: string, payload: string) {
|
|
||||||
const parsedPayload = payload ? (JSON.parse(payload) as unknown) : null
|
|
||||||
this.emitter.emit(eventName, parsedPayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Defines the expected contract of the host application bound object.
|
|
||||||
*/
|
|
||||||
export type IRawBridge = {
|
|
||||||
GetBindingsMethodNames: () => Promise<string[]>
|
|
||||||
RunMethod: (methodName: string, args: string) => Promise<string>
|
|
||||||
ShowDevTools: () => Promise<void>
|
|
||||||
OpenUrl: (url: string) => Promise<void>
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
constructor(object: IRawBridge) {
|
|
||||||
super()
|
|
||||||
this.bridge = object
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create(): Promise<boolean> {
|
|
||||||
// NOTE: GetMethods is a call to the .NET side.
|
|
||||||
let availableMethodNames = [] as string[]
|
|
||||||
|
|
||||||
try {
|
|
||||||
availableMethodNames = await this.bridge.GetBindingsMethodNames()
|
|
||||||
} catch {
|
|
||||||
console.warn(`Failed to get method names.`)
|
|
||||||
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) {
|
|
||||||
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 runMethod(methodName: string, args: unknown[]): Promise<unknown> {
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
const parsed = result ? (JSON.parse(result) as Record<string, unknown>) : null
|
|
||||||
|
|
||||||
if (parsed && parsed['error']) {
|
|
||||||
console.error(parsed)
|
|
||||||
throw new Error(
|
|
||||||
`Failed to run ${methodName} with args ${JSON.stringify(
|
|
||||||
args
|
|
||||||
)}. The host app error is logged above.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
public showDevTools() {
|
|
||||||
this.bridge.ShowDevTools()
|
|
||||||
}
|
|
||||||
|
|
||||||
public openUrl(url: string) {
|
|
||||||
this.bridge.OpenUrl(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowercaseMethodName = (name: string) =>
|
|
||||||
name.charAt(0).toLowerCase() + name.slice(1)
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { uniqueId } from 'lodash-es'
|
|
||||||
import { BaseBridge } from './base'
|
|
||||||
|
|
||||||
declare let sketchup: {
|
|
||||||
exec: (data: Record<string, unknown>) => void
|
|
||||||
getCommands: (viewId: string) => 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 ).
|
|
||||||
* 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 extends BaseBridge {
|
|
||||||
private requests = {} as Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
resolve: (value: unknown) => void
|
|
||||||
reject: (reason: string | Error) => void
|
|
||||||
rejectTimerId: number
|
|
||||||
}
|
|
||||||
>
|
|
||||||
private bindingName: string
|
|
||||||
private TIMEOUT_MS = 2000 // 2s
|
|
||||||
public isInitalized: Promise<boolean>
|
|
||||||
private resolveIsInitializedPromise!: (v: boolean) => unknown
|
|
||||||
private rejectIsInitializedPromise!: (message: string) => unknown
|
|
||||||
|
|
||||||
constructor(bindingName: string) {
|
|
||||||
super()
|
|
||||||
this.bindingName = bindingName || 'default_bindings'
|
|
||||||
|
|
||||||
this.isInitalized = new Promise((resolve, reject) => {
|
|
||||||
this.resolveIsInitializedPromise = resolve
|
|
||||||
this.rejectIsInitializedPromise = reject
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
reject(
|
|
||||||
`Failed to get command names from Sketchup; timed out after ${this.TIMEOUT_MS}ms.`
|
|
||||||
),
|
|
||||||
this.TIMEOUT_MS
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// NOTE: we need to hoist the bindings in global scope BEFORE we call sketchup exec get comands below.
|
|
||||||
;(globalThis as Record<string, unknown>).bindings = this
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 as unknown as Record<string, unknown>)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async create(): Promise<boolean> {
|
|
||||||
// Initialization continues in the receiveCommandsAndInitializeBridge function,
|
|
||||||
// where we expect sketchup to return to us the command names for related bindings/views.
|
|
||||||
sketchup.getCommands(this.bindingName)
|
|
||||||
|
|
||||||
//
|
|
||||||
try {
|
|
||||||
await this.isInitalized
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(commandNamesString: string) {
|
|
||||||
const commandNames = JSON.parse(commandNamesString) as string[]
|
|
||||||
const hoistTarget = this as unknown as Record<string, unknown>
|
|
||||||
for (const commandName of commandNames) {
|
|
||||||
hoistTarget[commandName] = (...args: unknown[]) =>
|
|
||||||
this.runMethod(commandName, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resolveIsInitializedPromise(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will be called by `executeScript('bindings.rejectBindings()')` from sketchup.
|
|
||||||
* @param message
|
|
||||||
*/
|
|
||||||
private rejectBindings(message: string) {
|
|
||||||
this.rejectIsInitializedPromise(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal calls to Sketchup.
|
|
||||||
* @param methodName
|
|
||||||
* @param args
|
|
||||||
*/
|
|
||||||
private async runMethod(methodName: string, args: unknown[]): Promise<unknown> {
|
|
||||||
const requestId = uniqueId(this.bindingName)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
request_id: requestId,
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
binding_name: this.bindingName,
|
|
||||||
data: { 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 (${this.TIMEOUT_MS}ms).`
|
|
||||||
)
|
|
||||||
delete this.requests[requestId]
|
|
||||||
}, this.TIMEOUT_MS)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private receiveResponse(requestId: string, data: object) {
|
|
||||||
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 {
|
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
|
||||||
if (data && data.hasOwnProperty('error')) {
|
|
||||||
console.error(data)
|
|
||||||
throw new Error(
|
|
||||||
`Failed to run ${requestId}. The host app error is logged above.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE/TODO: does not need parsing
|
|
||||||
// const parsedData = JSON.parse(data) as Record<string, unknown> // TODO: check if data is undefined
|
|
||||||
request.resolve(data)
|
|
||||||
} catch (e) {
|
|
||||||
request.reject(e as Error)
|
|
||||||
} finally {
|
|
||||||
window.clearTimeout(request.rejectTimerId)
|
|
||||||
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.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
import * as types from './graphql';
|
|
||||||
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of all GraphQL operations in the project.
|
|
||||||
*
|
|
||||||
* This map has several performance disadvantages:
|
|
||||||
* 1. It is not tree-shakeable, so it will include all operations in the project.
|
|
||||||
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
|
|
||||||
* 3. It does not support dead code elimination, so it will add unused operations.
|
|
||||||
*
|
|
||||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
|
||||||
*/
|
|
||||||
const documents = {
|
|
||||||
"\n query AcccountTestQuery {\n serverInfo {\n version\n name\n company\n }\n }\n ": types.AcccountTestQueryDocument,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The query argument is unknown!
|
|
||||||
* Please regenerate the types.
|
|
||||||
*/
|
|
||||||
export function graphql(source: string): unknown;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
|
||||||
*/
|
|
||||||
export function graphql(source: "\n query AcccountTestQuery {\n serverInfo {\n version\n name\n company\n }\n }\n "): (typeof documents)["\n query AcccountTestQuery {\n serverInfo {\n version\n name\n company\n }\n }\n "];
|
|
||||||
|
|
||||||
export function graphql(source: string) {
|
|
||||||
return (documents as any)[source] ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
export * from "./gql";
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
||||||
import type { ApolloLink, ApolloClientOptions } from '@apollo/client/core'
|
|
||||||
import { InMemoryCache, split, from } from '@apollo/client/core'
|
|
||||||
import { setContext } from '@apollo/client/link/context'
|
|
||||||
import { SubscriptionClient } from 'subscriptions-transport-ws'
|
|
||||||
import { createUploadLink } from 'apollo-upload-client'
|
|
||||||
import { WebSocketLink } from '@apollo/client/link/ws'
|
|
||||||
import { getMainDefinition } from '@apollo/client/utilities'
|
|
||||||
import type { OperationDefinitionNode } from 'graphql'
|
|
||||||
import { Kind } from 'graphql'
|
|
||||||
import type { Nullable } from '@speckle/shared'
|
|
||||||
import {
|
|
||||||
buildAbstractCollectionMergeFunction,
|
|
||||||
buildArrayMergeFunction,
|
|
||||||
incomingOverwritesExistingMergeFunction
|
|
||||||
} from '~~/lib/core/helpers/apolloSetup'
|
|
||||||
import { onError } from '@apollo/client/link/error'
|
|
||||||
import * as Observability from '@speckle/shared/dist/esm/observability/index.js'
|
|
||||||
|
|
||||||
let subscriptionsStopped = false
|
|
||||||
const errorRpm = Observability.simpleRpmCounter()
|
|
||||||
const STOP_SUBSCRIPTIONS_AT_ERRORS_PER_MIN = 100
|
|
||||||
|
|
||||||
const appVersion = (import.meta.env.SPECKLE_SERVER_VERSION as string) || 'unknown'
|
|
||||||
const appName = 'dui-3'
|
|
||||||
|
|
||||||
function createCache(): InMemoryCache {
|
|
||||||
return new InMemoryCache({
|
|
||||||
/**
|
|
||||||
* This is where you configure how various GQL fields should be read, written to or merged when new data comes in.
|
|
||||||
* If you define a merge function here, you don't need to duplicate the merge logic inside an `update()` callback
|
|
||||||
* of a fetchMore call, for example.
|
|
||||||
*
|
|
||||||
* Feel free to re-use utilities in the `apolloSetup` helper for defining merge functions or even use the ones that come from `@apollo/client/utilities`.
|
|
||||||
*
|
|
||||||
* Read more: https://www.apollographql.com/docs/react/caching/cache-field-behavior
|
|
||||||
*/
|
|
||||||
typePolicies: {
|
|
||||||
Query: {
|
|
||||||
fields: {
|
|
||||||
otherUser: {
|
|
||||||
read(original, { args, toReference }) {
|
|
||||||
if (args?.id) {
|
|
||||||
return toReference({ __typename: 'LimitedUser', id: args.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return original
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activeUser: {
|
|
||||||
merge(existing, incoming, { mergeObjects }) {
|
|
||||||
return mergeObjects(existing, incoming)
|
|
||||||
},
|
|
||||||
read(original, { args, toReference }) {
|
|
||||||
if (args?.id) {
|
|
||||||
return toReference({ __typename: 'User', id: args.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return original
|
|
||||||
}
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
read(original, { args, toReference }) {
|
|
||||||
if (args?.id) {
|
|
||||||
return toReference({ __typename: 'User', id: args.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return original
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stream: {
|
|
||||||
read(original, { args, toReference }) {
|
|
||||||
if (args?.id) {
|
|
||||||
return toReference({ __typename: 'Stream', id: args.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return original
|
|
||||||
}
|
|
||||||
},
|
|
||||||
streams: {
|
|
||||||
keyArgs: ['query'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('UserStreamCollection', {
|
|
||||||
checkIdentity: true
|
|
||||||
})
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
read(original, { args, toReference }) {
|
|
||||||
if (args?.id) {
|
|
||||||
return toReference({ __typename: 'Project', id: args.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return original
|
|
||||||
}
|
|
||||||
},
|
|
||||||
projects: {
|
|
||||||
merge: buildArrayMergeFunction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LimitedUser: {
|
|
||||||
fields: {
|
|
||||||
commits: {
|
|
||||||
keyArgs: false,
|
|
||||||
merge: buildAbstractCollectionMergeFunction('CommitCollection')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
User: {
|
|
||||||
fields: {
|
|
||||||
timeline: {
|
|
||||||
keyArgs: ['after', 'before'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
|
||||||
},
|
|
||||||
commits: {
|
|
||||||
keyArgs: false,
|
|
||||||
merge: buildAbstractCollectionMergeFunction('CommitCollection')
|
|
||||||
},
|
|
||||||
favoriteStreams: {
|
|
||||||
keyArgs: false,
|
|
||||||
merge: buildAbstractCollectionMergeFunction('StreamCollection')
|
|
||||||
},
|
|
||||||
projects: {
|
|
||||||
keyArgs: ['filter', 'limit'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('ProjectCollection')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Project: {
|
|
||||||
fields: {
|
|
||||||
models: {
|
|
||||||
keyArgs: ['filter', 'limit'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('ModelCollection')
|
|
||||||
},
|
|
||||||
versions: {
|
|
||||||
keyArgs: ['filter', 'limit'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('VersionCollection')
|
|
||||||
},
|
|
||||||
commentThreads: {
|
|
||||||
keyArgs: ['filter', 'limit'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('CommentCollection')
|
|
||||||
},
|
|
||||||
modelsTree: {
|
|
||||||
keyArgs: ['filter', 'limit'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('ModelsTreeItemCollection')
|
|
||||||
},
|
|
||||||
replyAuthors: {
|
|
||||||
keyArgs: false,
|
|
||||||
merge: buildAbstractCollectionMergeFunction('CommentReplyAuthorCollection')
|
|
||||||
},
|
|
||||||
viewerResources: {
|
|
||||||
merge: (_existing, incoming) => [...incoming]
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
read(original, { args, toReference }) {
|
|
||||||
if (args?.id) {
|
|
||||||
return toReference({ __typename: 'Model', id: args.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
return original
|
|
||||||
}
|
|
||||||
},
|
|
||||||
team: {
|
|
||||||
merge: (_existing, incoming) => incoming
|
|
||||||
},
|
|
||||||
invitedTeam: {
|
|
||||||
merge: (_existing, incoming) => incoming
|
|
||||||
},
|
|
||||||
pendingImportedModels: {
|
|
||||||
merge: (_existing, incoming) => incoming
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Model: {
|
|
||||||
fields: {
|
|
||||||
versions: {
|
|
||||||
keyArgs: ['filter', 'limit'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('VersionCollection')
|
|
||||||
},
|
|
||||||
pendingImportedVersions: {
|
|
||||||
merge: (_existing, incoming) => incoming
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Comment: {
|
|
||||||
fields: {
|
|
||||||
replies: {
|
|
||||||
keyArgs: ['limit']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Stream: {
|
|
||||||
fields: {
|
|
||||||
activity: {
|
|
||||||
keyArgs: ['after', 'before', 'actionType'],
|
|
||||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
|
||||||
},
|
|
||||||
commits: {
|
|
||||||
keyArgs: false,
|
|
||||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
|
||||||
checkIdentity: true
|
|
||||||
})
|
|
||||||
},
|
|
||||||
pendingCollaborators: {
|
|
||||||
merge: incomingOverwritesExistingMergeFunction
|
|
||||||
},
|
|
||||||
pendingAccessRequests: {
|
|
||||||
merge: incomingOverwritesExistingMergeFunction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Branch: {
|
|
||||||
fields: {
|
|
||||||
commits: {
|
|
||||||
keyArgs: false,
|
|
||||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
|
||||||
checkIdentity: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
BranchCollection: {
|
|
||||||
merge: true
|
|
||||||
},
|
|
||||||
ServerStats: {
|
|
||||||
merge: true
|
|
||||||
},
|
|
||||||
WebhookEventCollection: {
|
|
||||||
merge: true
|
|
||||||
},
|
|
||||||
ServerInfo: {
|
|
||||||
merge: true
|
|
||||||
},
|
|
||||||
CommentThreadActivityMessage: {
|
|
||||||
merge: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWsClient(params: {
|
|
||||||
wsEndpoint: string
|
|
||||||
authToken: () => Nullable<string>
|
|
||||||
}): SubscriptionClient {
|
|
||||||
const { wsEndpoint, authToken } = params
|
|
||||||
|
|
||||||
return new SubscriptionClient(wsEndpoint, {
|
|
||||||
reconnect: true,
|
|
||||||
reconnectionAttempts: 3,
|
|
||||||
connectionParams: () => {
|
|
||||||
const token = authToken()
|
|
||||||
const Authorization = token?.length ? `Bearer ${token}` : null
|
|
||||||
return Authorization ? { Authorization, headers: { Authorization } } : {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function createLink(params: {
|
|
||||||
httpEndpoint: string
|
|
||||||
wsClient?: SubscriptionClient
|
|
||||||
authToken: () => Nullable<string>
|
|
||||||
}): ApolloLink {
|
|
||||||
const { httpEndpoint, wsClient, authToken } = params
|
|
||||||
// Prepare links
|
|
||||||
const httpLink = createUploadLink({
|
|
||||||
uri: httpEndpoint
|
|
||||||
})
|
|
||||||
|
|
||||||
const authLink = setContext((_, { headers }) => {
|
|
||||||
const token = authToken()
|
|
||||||
const authHeader = token?.length ? { Authorization: `Bearer ${token}` } : {}
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
...authHeader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let link = authLink.concat(httpLink as unknown as ApolloLink)
|
|
||||||
|
|
||||||
if (wsClient) {
|
|
||||||
const wsLink = new WebSocketLink(wsClient)
|
|
||||||
link = split(
|
|
||||||
({ query }) => {
|
|
||||||
const definition = getMainDefinition(query) as OperationDefinitionNode
|
|
||||||
const { kind, operation } = definition
|
|
||||||
|
|
||||||
return kind === Kind.OPERATION_DEFINITION && operation === 'subscription'
|
|
||||||
},
|
|
||||||
wsLink,
|
|
||||||
link
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorLink = onError((res) => {
|
|
||||||
console.error('Apollo Client error', res)
|
|
||||||
|
|
||||||
// Disable subscriptions if too many errors per minute
|
|
||||||
const rpm = errorRpm.hit()
|
|
||||||
if (
|
|
||||||
import.meta.client &&
|
|
||||||
wsClient &&
|
|
||||||
!subscriptionsStopped &&
|
|
||||||
rpm > STOP_SUBSCRIPTIONS_AT_ERRORS_PER_MIN
|
|
||||||
) {
|
|
||||||
subscriptionsStopped = true
|
|
||||||
console.error(
|
|
||||||
`Too many errors (${rpm} errors per minute), stopping subscriptions!`
|
|
||||||
)
|
|
||||||
wsClient.use([
|
|
||||||
{
|
|
||||||
applyMiddleware: () => {
|
|
||||||
// never invokes next() - essentially stuck
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return from([errorLink, link])
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResolveClientConfigParams = {
|
|
||||||
httpEndpoint: string
|
|
||||||
authToken: () => Nullable<string>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const resolveClientConfig = (
|
|
||||||
params: ResolveClientConfigParams
|
|
||||||
): Pick<ApolloClientOptions<unknown>, 'cache' | 'link' | 'name' | 'version'> => {
|
|
||||||
const { httpEndpoint, authToken } = params
|
|
||||||
const wsEndpoint = httpEndpoint.replace('http', 'ws')
|
|
||||||
|
|
||||||
const wsClient = import.meta.client
|
|
||||||
? createWsClient({ wsEndpoint, authToken })
|
|
||||||
: undefined
|
|
||||||
const link = createLink({ httpEndpoint, wsClient, authToken })
|
|
||||||
|
|
||||||
return {
|
|
||||||
// If we don't markRaw the cache, sometimes we get cryptic internal Apollo Client errors that essentially
|
|
||||||
// result from parts of its internals being made reactive, even tho they shouldn't be
|
|
||||||
cache: markRaw(createCache()),
|
|
||||||
link,
|
|
||||||
name: appName,
|
|
||||||
version: appVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import type { Optional } from '@speckle/shared'
|
|
||||||
import type { FieldMergeFunction } from '@apollo/client/core'
|
|
||||||
|
|
||||||
interface AbstractCollection<T extends string> {
|
|
||||||
__typename: T
|
|
||||||
totalCount: number
|
|
||||||
cursor: string | null
|
|
||||||
items: Record<string, unknown>[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MergeSettings {
|
|
||||||
/**
|
|
||||||
* Set to false if you want to merge incoming items without checking
|
|
||||||
* for duplicates. Usually you don't want to do this as you can introduce duplicates this way.
|
|
||||||
* Defaults to true
|
|
||||||
*/
|
|
||||||
checkIdentity: boolean
|
|
||||||
/**
|
|
||||||
* Optionally change the prop that should be used to compare
|
|
||||||
* equality between items
|
|
||||||
* Defaults to '__ref', which is the prop added by Apollo that contains the globally unique ID of the object
|
|
||||||
*/
|
|
||||||
identityProp: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const prepareMergeSettings = (
|
|
||||||
settings: Optional<Partial<MergeSettings>>
|
|
||||||
): MergeSettings => ({
|
|
||||||
checkIdentity: true,
|
|
||||||
identityProp: '__ref',
|
|
||||||
...(settings || {})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an Apollo merge function for a field that returns an array of identifiable objects
|
|
||||||
*/
|
|
||||||
export function buildArrayMergeFunction(
|
|
||||||
settings?: Partial<MergeSettings>
|
|
||||||
): FieldMergeFunction<Record<string, unknown>[], Record<string, unknown>[]> {
|
|
||||||
const { checkIdentity, identityProp } = prepareMergeSettings(settings)
|
|
||||||
return (existing, incoming) => {
|
|
||||||
let finalItems: Record<string, unknown>[]
|
|
||||||
if (checkIdentity) {
|
|
||||||
finalItems = [...(existing || [])]
|
|
||||||
for (const newItem of incoming || []) {
|
|
||||||
if (
|
|
||||||
finalItems.findIndex(
|
|
||||||
(item) => item[identityProp] === newItem[identityProp]
|
|
||||||
) === -1
|
|
||||||
) {
|
|
||||||
finalItems.push(newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
finalItems = [...(existing || []), ...(incoming || [])]
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalItems
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an Apollo merge function for a field that returns a collection like AbstractCollection
|
|
||||||
*/
|
|
||||||
export function buildAbstractCollectionMergeFunction<T extends string>(
|
|
||||||
typeName: T,
|
|
||||||
settings?: Partial<MergeSettings>
|
|
||||||
): FieldMergeFunction<Optional<AbstractCollection<T>>, AbstractCollection<T>> {
|
|
||||||
const { checkIdentity, identityProp } = prepareMergeSettings(settings)
|
|
||||||
return (
|
|
||||||
existing: Optional<AbstractCollection<T>>,
|
|
||||||
incoming: AbstractCollection<T>
|
|
||||||
) => {
|
|
||||||
const existingItems = existing?.items || []
|
|
||||||
const incomingItems = incoming?.items || []
|
|
||||||
|
|
||||||
let finalItems: Record<string, unknown>[]
|
|
||||||
if (checkIdentity) {
|
|
||||||
finalItems = [...existingItems]
|
|
||||||
for (const newItem of incomingItems) {
|
|
||||||
if (
|
|
||||||
finalItems.findIndex(
|
|
||||||
(item) => item[identityProp] === newItem[identityProp]
|
|
||||||
) === -1
|
|
||||||
) {
|
|
||||||
finalItems.push(newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
finalItems = [...existingItems, ...incomingItems]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
__typename: incoming?.__typename || existing?.__typename || typeName,
|
|
||||||
totalCount: incoming.totalCount || 0,
|
|
||||||
cursor: incoming.cursor || null,
|
|
||||||
items: finalItems
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge function that just takes incoming data and overrides all of old data with it
|
|
||||||
* Useful for array fields w/o pagination, where a new array response is supposed to replace
|
|
||||||
* the entire old one
|
|
||||||
*/
|
|
||||||
export const incomingOverwritesExistingMergeFunction: FieldMergeFunction = (
|
|
||||||
_existing: unknown,
|
|
||||||
incoming: unknown
|
|
||||||
) => incoming
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import legacy from '@vitejs/plugin-legacy'
|
|
||||||
|
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
||||||
export default defineNuxtConfig({
|
|
||||||
typescript: {
|
|
||||||
shim: false,
|
|
||||||
strict: true
|
|
||||||
},
|
|
||||||
modules: [
|
|
||||||
'@nuxt/eslint',
|
|
||||||
'@nuxtjs/tailwindcss',
|
|
||||||
'@speckle/ui-components-nuxt',
|
|
||||||
'@pinia/nuxt'
|
|
||||||
],
|
|
||||||
alias: {
|
|
||||||
// Rewriting all lodash calls to lodash-es for proper tree-shaking & chunk splitting
|
|
||||||
lodash: 'lodash-es'
|
|
||||||
},
|
|
||||||
|
|
||||||
vite: {
|
|
||||||
resolve: {
|
|
||||||
alias: [{ find: /^lodash$/, replacement: 'lodash-es' }]
|
|
||||||
},
|
|
||||||
|
|
||||||
build: {
|
|
||||||
// older chrome version for CEF 65 support. all identifiers except the chrome one are default ones.
|
|
||||||
target: ['es2020', 'edge88', 'firefox78', 'chrome65', 'safari14'],
|
|
||||||
// optionally disable minification for debugging
|
|
||||||
minify: false
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// again - only for CEF 65
|
|
||||||
legacy({
|
|
||||||
renderLegacyChunks: false,
|
|
||||||
// only adding the specific polyfills we need to reduce bundle size
|
|
||||||
modernPolyfills: ['es.global-this', 'es/object', 'es/array']
|
|
||||||
})
|
|
||||||
]
|
|
||||||
},
|
|
||||||
ssr: false,
|
|
||||||
build: {
|
|
||||||
transpile: [
|
|
||||||
/^@apollo\/client/,
|
|
||||||
'ts-invariant/process',
|
|
||||||
'@vue/apollo-composable',
|
|
||||||
'@headlessui/vue',
|
|
||||||
/^@heroicons\/vue/,
|
|
||||||
'@vueuse/core',
|
|
||||||
'@vueuse/shared',
|
|
||||||
'@speckle/ui-components'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
hooks: {
|
|
||||||
'build:manifest': (manifest) => {
|
|
||||||
// kinda hacky, vite polyfills are incorrectly being loaded last so we have to move them to appear first in the object.
|
|
||||||
// we can't replace `manifest` entirely, cause then we're only mutating a local variable, not the actual manifest
|
|
||||||
// which is why we have to mutate the reference.
|
|
||||||
// since ES2015 object string property order is more or less guaranteed - the order is chronological
|
|
||||||
const polyfillKey = 'vite/legacy-polyfills'
|
|
||||||
const polyfillEntry = manifest[polyfillKey]
|
|
||||||
if (!polyfillEntry) return
|
|
||||||
|
|
||||||
const oldManifest = { ...manifest }
|
|
||||||
delete oldManifest[polyfillKey]
|
|
||||||
|
|
||||||
for (const key in manifest) {
|
|
||||||
delete manifest[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest[polyfillKey] = polyfillEntry
|
|
||||||
for (const key in oldManifest) {
|
|
||||||
manifest[key] = oldManifest[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@speckle/dui3",
|
|
||||||
"description": "Speckle desktop UI embedded in connectors. Built w/ Vue 3 & Nuxt 3",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"private": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^22.6.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "nuxt build",
|
|
||||||
"dev:nuxt": "nuxt dev",
|
|
||||||
"dev": "concurrently \"nuxt dev\" \"yarn gqlgen:watch\"",
|
|
||||||
"generate": "nuxt generate",
|
|
||||||
"preview": "nuxt preview",
|
|
||||||
"postinstall": "yarn ensure:tailwind-deps && nuxt prepare",
|
|
||||||
"lint:js": "eslint .",
|
|
||||||
"lint:tsc": "vue-tsc --noEmit",
|
|
||||||
"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",
|
|
||||||
"gqlgen": "graphql-codegen",
|
|
||||||
"gqlgen:watch": "graphql-codegen --watch"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@apollo/client": "^3.7.14",
|
|
||||||
"@headlessui/vue": "^1.7.13",
|
|
||||||
"@heroicons/vue": "^2.0.12",
|
|
||||||
"@pinia/nuxt": "^0.4.11",
|
|
||||||
"@speckle/shared": "workspace:^",
|
|
||||||
"@speckle/ui-components": "workspace:^",
|
|
||||||
"@speckle/ui-components-nuxt": "workspace:^",
|
|
||||||
"@vue/apollo-composable": "^4.0.0-beta.5",
|
|
||||||
"@vueuse/core": "^9.13.0",
|
|
||||||
"apollo-upload-client": "^17.0.0",
|
|
||||||
"graphql": "^16.6.0",
|
|
||||||
"graphql-tag": "^2.12.6",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"nanoevents": "^8.0.0",
|
|
||||||
"pinia": "^2.1.4",
|
|
||||||
"portal-vue": "^3.0.0",
|
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
|
||||||
"vue-tippy": "^6.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@graphql-codegen/cli": "^5.0.5",
|
|
||||||
"@graphql-codegen/client-preset": "^4.3.0",
|
|
||||||
"@nuxt/eslint": "^0.3.13",
|
|
||||||
"@nuxtjs/tailwindcss": "^6.7.0",
|
|
||||||
"@parcel/watcher": "^2.5.1",
|
|
||||||
"@types/apollo-upload-client": "^17.0.1",
|
|
||||||
"@types/eslint": "^8.56.10",
|
|
||||||
"@types/lodash-es": "^4.17.6",
|
|
||||||
"@types/node": "^18",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.12.0",
|
|
||||||
"@typescript-eslint/parser": "^7.12.0",
|
|
||||||
"@vitejs/plugin-legacy": "^4.0.3",
|
|
||||||
"autoprefixer": "^10.4.14",
|
|
||||||
"concurrently": "^7.5.0",
|
|
||||||
"eslint": "^9.4.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-vuejs-accessibility": "^2.3.0",
|
|
||||||
"nuxt": "^3.6.3",
|
|
||||||
"postcss": "^8.4.31",
|
|
||||||
"postcss-custom-properties": "^12.1.9",
|
|
||||||
"postcss-html": "^1.5.0",
|
|
||||||
"postcss-nesting": "^10.2.0",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"stylelint": "^15.10.1",
|
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
|
||||||
"stylelint-config-recommended-vue": "^1.4.0",
|
|
||||||
"stylelint-config-standard": "^26.0.0",
|
|
||||||
"tailwindcss": "^3.3.2",
|
|
||||||
"type-fest": "^3.5.1",
|
|
||||||
"typescript": "^4.8.3",
|
|
||||||
"vue-tsc": "^2.2.2"
|
|
||||||
},
|
|
||||||
"installConfig": {
|
|
||||||
"hoistingLimits": "workspaces"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex items-center justify-center h-[calc(100vh-14rem)]">
|
|
||||||
<div
|
|
||||||
class="p-2 bg-primary text-foreground-on-primary shadow-md rounded-md font-bold"
|
|
||||||
>
|
|
||||||
TODO: Everything
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts"></script>
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<Portal to="navigation">
|
|
||||||
<FormButton to="/" size="sm" :icon-left="ArrowLeftIcon" class="ml-2">
|
|
||||||
Back home
|
|
||||||
</FormButton>
|
|
||||||
</Portal>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm text-foreground-2 py-2 px-2">
|
|
||||||
Do not expect these to save the day. They are just some
|
|
||||||
<b class="text-foreground-primary">minor sanity checks</b>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<FormButton
|
|
||||||
size="xl"
|
|
||||||
color="card"
|
|
||||||
full-width
|
|
||||||
class="sticky top-10 top-16"
|
|
||||||
@click="runTests()"
|
|
||||||
>
|
|
||||||
Run Tests
|
|
||||||
</FormButton>
|
|
||||||
<div
|
|
||||||
v-for="test in tests"
|
|
||||||
:key="test.name"
|
|
||||||
class="py-2 px-2 bg-foundation shadow hover:shadow-lg transition rounded-lg text-xs"
|
|
||||||
>
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<div>
|
|
||||||
<MinusIcon v-if="test.status === 0" class="w-4 h-4 text-primary" />
|
|
||||||
<CheckIcon v-if="test.status === 1" class="w-4 h-4 text-success" />
|
|
||||||
<XMarkIcon v-if="test.status === 2" class="w-4 h-4 text-danger" />
|
|
||||||
</div>
|
|
||||||
<div>{{ test.name }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs max-w-full overflow-x-scroll simple-scrollbar py-2">
|
|
||||||
<pre>{{ test }}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ArrowLeftIcon, CheckIcon, MinusIcon, XMarkIcon } from '@heroicons/vue/20/solid'
|
|
||||||
import type { TestEventArgs } from '~/lib/bindings/definitions/ITestBinding'
|
|
||||||
const { $testBindings } = useNuxtApp()
|
|
||||||
|
|
||||||
const tests = ref([
|
|
||||||
{
|
|
||||||
name: 'Simple call with parameters',
|
|
||||||
test: async (): Promise<unknown> => {
|
|
||||||
await $testBindings.sayHi('Speckle', 3, false)
|
|
||||||
return 'ok'
|
|
||||||
},
|
|
||||||
status: 0,
|
|
||||||
result: {} as unknown
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Simple call with invalid parameters',
|
|
||||||
test: async (): Promise<unknown> => {
|
|
||||||
try {
|
|
||||||
await (
|
|
||||||
$testBindings as unknown as {
|
|
||||||
sayHi: (name: string, count: number) => Promise<string>
|
|
||||||
}
|
|
||||||
).sayHi('Speckle', 0) // note, invalid on purpose, it looks long because ts needs to be happy
|
|
||||||
return 'not ok'
|
|
||||||
} catch {
|
|
||||||
return 'ok'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
status: 0,
|
|
||||||
result: {} as unknown
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Simple function call with no args and no result',
|
|
||||||
test: async (): Promise<unknown> => {
|
|
||||||
const res = await $testBindings.goAway()
|
|
||||||
return res === null || res === undefined ? 'ok' : 'not ok'
|
|
||||||
},
|
|
||||||
status: 0,
|
|
||||||
result: {} as unknown
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Get a more complicated object from a method call',
|
|
||||||
test: async (): Promise<unknown> => {
|
|
||||||
const res = await $testBindings.getComplexType()
|
|
||||||
const key = Object.keys(res)[0]
|
|
||||||
|
|
||||||
return key.toLowerCase()[0] === key[0] ? 'ok' : 'serialization gone wrong'
|
|
||||||
},
|
|
||||||
status: 0,
|
|
||||||
result: {} as unknown
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Simple event capture',
|
|
||||||
test: async () => {
|
|
||||||
await $testBindings.triggerEvent('emptyTestEvent')
|
|
||||||
return 'not ok'
|
|
||||||
},
|
|
||||||
status: 0,
|
|
||||||
result: 'not run yet' as unknown
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Event capture with args',
|
|
||||||
test: async () => {
|
|
||||||
await $testBindings.triggerEvent('testEvent')
|
|
||||||
return 'not ok'
|
|
||||||
},
|
|
||||||
status: 0,
|
|
||||||
result: 'not run yet' as unknown
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const runTests = async () => {
|
|
||||||
for (const test of tests.value) {
|
|
||||||
test.result = null
|
|
||||||
test.status = 0
|
|
||||||
}
|
|
||||||
for (const test of tests.value) {
|
|
||||||
try {
|
|
||||||
const res = await test.test()
|
|
||||||
if (res === 'ok') {
|
|
||||||
test.status = 1
|
|
||||||
} else {
|
|
||||||
test.status = 2
|
|
||||||
}
|
|
||||||
test.result = res
|
|
||||||
} catch (e) {
|
|
||||||
test.status = 2
|
|
||||||
test.result = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$testBindings.on('emptyTestEvent', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('sketchup sent event back', 'emptyTestEvent')
|
|
||||||
|
|
||||||
const myTest = tests.value.find((t) => t.name === 'Simple event capture')
|
|
||||||
console.log(myTest, 'myTest')
|
|
||||||
|
|
||||||
if (!myTest) return
|
|
||||||
myTest.status = 1
|
|
||||||
myTest.result = 'got an event back, we are okay'
|
|
||||||
}, 300)
|
|
||||||
})
|
|
||||||
|
|
||||||
$testBindings.on('testEvent', (args: TestEventArgs) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log(args, 'testEvent')
|
|
||||||
const myTest = tests.value.find((t) => t.name === 'Event capture with args')
|
|
||||||
console.log(myTest, 'myTest')
|
|
||||||
if (!myTest) return
|
|
||||||
myTest.status = 1
|
|
||||||
myTest.result = args
|
|
||||||
}, 300)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import type { IRawBridge } from '~/lib/bridge/definitions'
|
|
||||||
|
|
||||||
import { GenericBridge } from '~/lib/bridge/generic'
|
|
||||||
import { SketchupBridge } from '~/lib/bridge/sketchup'
|
|
||||||
|
|
||||||
import type { IBasicConnectorBinding } from '~/lib/bindings/definitions/IBasicConnectorBinding'
|
|
||||||
import {
|
|
||||||
IBasicConnectorBindingKey,
|
|
||||||
MockedBaseBinding
|
|
||||||
} from '~/lib/bindings/definitions/IBasicConnectorBinding'
|
|
||||||
|
|
||||||
import type { ITestBinding } from '~/lib/bindings/definitions/ITestBinding'
|
|
||||||
import {
|
|
||||||
ITestBindingKey,
|
|
||||||
MockedTestBinding
|
|
||||||
} from '~/lib/bindings/definitions/ITestBinding'
|
|
||||||
|
|
||||||
import type { IConfigBinding } from '~/lib/bindings/definitions/IConfigBinding'
|
|
||||||
import {
|
|
||||||
IConfigBindingKey,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
MockedConfigBinding
|
|
||||||
} from '~/lib/bindings/definitions/IConfigBinding'
|
|
||||||
|
|
||||||
// Makes TS happy
|
|
||||||
declare let globalThis: Record<string, unknown> & {
|
|
||||||
CefSharp?: { BindObjectAsync: (name: string) => Promise<void> }
|
|
||||||
chrome?: { webview: { hostObjects: Record<string, IRawBridge> } }
|
|
||||||
sketchup?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 () => {
|
|
||||||
// Registers some default test bindings.
|
|
||||||
const testBindings =
|
|
||||||
(await tryHoistBinding<ITestBinding>(ITestBindingKey)) || new MockedTestBinding()
|
|
||||||
|
|
||||||
// Tries to register some non-existant bindings.
|
|
||||||
const nonExistantBindings = await tryHoistBinding('nonExistantBindings')
|
|
||||||
|
|
||||||
// Registers a set of default bindings.
|
|
||||||
const baseBinding =
|
|
||||||
(await tryHoistBinding<IBasicConnectorBinding>(IBasicConnectorBindingKey)) ||
|
|
||||||
new MockedBaseBinding()
|
|
||||||
|
|
||||||
// UI configuration bindings.
|
|
||||||
const configBinding = await tryHoistBinding<IConfigBinding>(IConfigBindingKey)
|
|
||||||
|
|
||||||
const showDevTools = () => {
|
|
||||||
baseBinding.showDevTools()
|
|
||||||
}
|
|
||||||
|
|
||||||
const openUrl = (url: string) => {
|
|
||||||
baseBinding.openUrl(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
testBindings,
|
|
||||||
nonExistantBindings,
|
|
||||||
baseBinding,
|
|
||||||
configBinding,
|
|
||||||
showDevTools,
|
|
||||||
openUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 <T>(name: string) => {
|
|
||||||
let bridge: GenericBridge | SketchupBridge | null = null
|
|
||||||
let tempBridge: GenericBridge | SketchupBridge | null = null
|
|
||||||
|
|
||||||
if (globalThis.CefSharp) {
|
|
||||||
await globalThis.CefSharp.BindObjectAsync(name)
|
|
||||||
tempBridge = new GenericBridge(globalThis[name] as unknown as IRawBridge)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (globalThis.chrome && globalThis.chrome.webview && !tempBridge) {
|
|
||||||
tempBridge = new GenericBridge(globalThis.chrome.webview.hostObjects[name])
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
return bridge as unknown as T
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import PortalVue from 'portal-vue'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.use(PortalVue)
|
|
||||||
})
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import VueTippy from 'vue-tippy'
|
|
||||||
import 'tippy.js/dist/tippy.css'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.use(VueTippy, {
|
|
||||||
defaultProps: {
|
|
||||||
arrow: true
|
|
||||||
},
|
|
||||||
flipDuration: 0
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
'postcss-nesting': {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../.nuxt/tsconfig.server.json"
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import { useInjectedAccounts } from '~/lib/accounts/composables/setup'
|
|
||||||
|
|
||||||
// NOTE: this store simply wraps around the injected accounts composable (for now)
|
|
||||||
export const useAccountStore = defineStore('accountStore', () => {
|
|
||||||
const { accounts, refreshAccounts, defaultAccount, validAccounts, loading } =
|
|
||||||
useInjectedAccounts()
|
|
||||||
|
|
||||||
return { accounts, refreshAccounts, defaultAccount, validAccounts, loading }
|
|
||||||
})
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import type { DocumentInfo } from 'lib/bindings/definitions/IBasicConnectorBinding'
|
|
||||||
|
|
||||||
export const useDocumentInfoStore = defineStore('documentInfoStore', () => {
|
|
||||||
const app = useNuxtApp()
|
|
||||||
const documentInfo = ref<DocumentInfo>()
|
|
||||||
|
|
||||||
app.$baseBinding.on('documentChanged', () => {
|
|
||||||
console.log('doc changed')
|
|
||||||
setTimeout(async () => {
|
|
||||||
const docInfo = await app.$baseBinding.getDocumentInfo()
|
|
||||||
documentInfo.value = docInfo
|
|
||||||
}, 500) // Rhino needs some time.
|
|
||||||
})
|
|
||||||
|
|
||||||
const initDocInfo = async () => {
|
|
||||||
documentInfo.value = await app.$baseBinding.getDocumentInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
void initDocInfo()
|
|
||||||
|
|
||||||
return { documentInfo }
|
|
||||||
})
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import type { Config } from 'lib/bindings/definitions/IConfigBinding'
|
|
||||||
|
|
||||||
export const useDocumentInfoStore = defineStore('documentInfoStore', () => {
|
|
||||||
const { $configBinding } = useNuxtApp()
|
|
||||||
|
|
||||||
const hasConfigBindings = ref(!!$configBinding)
|
|
||||||
const uiConfig = ref<Config>({ 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 }
|
|
||||||
})
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
extends: [
|
|
||||||
'stylelint-config-standard',
|
|
||||||
'stylelint-config-recommended-vue',
|
|
||||||
'stylelint-config-prettier'
|
|
||||||
],
|
|
||||||
// add your custom config here
|
|
||||||
// https://stylelint.io/user-guide/configuration
|
|
||||||
rules: {
|
|
||||||
// Rules to make stylelint happy with tailwind syntax
|
|
||||||
'at-rule-no-unknown': [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'declaration-block-trailing-semicolon': null,
|
|
||||||
'no-descending-specificity': null
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: '**/*.vue',
|
|
||||||
rules: {
|
|
||||||
'value-keyword-case': null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { plugin as speckleTheme } 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'
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
const config = {
|
|
||||||
darkMode: 'class',
|
|
||||||
content: [
|
|
||||||
`./components/**/*.{vue,js,ts}`,
|
|
||||||
`./layouts/**/*.vue`,
|
|
||||||
`./pages/**/*.vue`,
|
|
||||||
`./composables/**/*.{js,ts}`,
|
|
||||||
`./plugins/**/*.{js,ts}`,
|
|
||||||
'./stories/**/*.{js,ts,vue,mdx}',
|
|
||||||
'./app.vue',
|
|
||||||
'./.storybook/**/*.{js,ts,vue}',
|
|
||||||
'./lib/**/composables/*.{js,ts}',
|
|
||||||
...themeEntries(),
|
|
||||||
...uiLibEntries()
|
|
||||||
// `./lib/**/*.{js,ts,vue}`, // TODO: Wait for fix https://github.com/nuxt/framework/issues/2886#issuecomment-1108312903
|
|
||||||
],
|
|
||||||
plugins: [speckleTheme, formsPlugin]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
// https://v3.nuxtjs.org/concepts/typescript
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": ["./.nuxt/nuxt.d.ts", "**/*", ".*.js"]
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
|
||||||
"extends": "./.nuxt/tsconfig.json"
|
|
||||||
}
|
|
||||||
@@ -8,10 +8,6 @@
|
|||||||
"path": "packages/frontend-2",
|
"path": "packages/frontend-2",
|
||||||
"name": "🏬 frontend"
|
"name": "🏬 frontend"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "packages/dui3",
|
|
||||||
"name": "🥉 dui 3.0"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "packages/tailwind-theme",
|
"path": "packages/tailwind-theme",
|
||||||
"name": "🎨 tailwind-theme"
|
"name": "🎨 tailwind-theme"
|
||||||
|
|||||||
Reference in New Issue
Block a user