Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 668e669b5b | |||
| 7b406c53a2 | |||
| 6b8cb8e94d | |||
| 8e2f507286 | |||
| 7bfdddd283 | |||
| a26d410c7b | |||
| 6b0990a364 | |||
| 428ff36568 | |||
| 9d3a623fe6 | |||
| 488dc66948 | |||
| 8589777081 | |||
| df93ebb50c | |||
| a539114b0e | |||
| 476a95b842 | |||
| c2f50cd71b | |||
| 53e65637e2 | |||
| 1585844f02 | |||
| 77d67798ca | |||
| 8fc81b0b4e | |||
| 6f2f599b1b | |||
| a69de13f16 | |||
| d2b0d35119 | |||
| b026659460 | |||
| 009cc77bab | |||
| a8b802b7e3 | |||
| 6fc3df4a0d | |||
| f47f19c02d | |||
| 85f806368a | |||
| 35ddce1f90 | |||
| a37b3389d6 | |||
| ed4aa92ce1 | |||
| 60f3bed254 | |||
| 2f412df64a | |||
| c7e0929eca | |||
| eef0a59719 | |||
| 19f306756c | |||
| 305b100d34 | |||
| f2cc0d55e3 | |||
| fdfef1d496 | |||
| 5174af78cc | |||
| ede6e99440 | |||
| 9c708c64a0 | |||
| 41e635c8ef | |||
| 095ccf114d | |||
| a95fd9bdfe | |||
| bc665a008c | |||
| 00a6a66ee0 | |||
| b0157af3c8 | |||
| 9b065bf921 | |||
| 99ebd403c7 | |||
| a166b86657 | |||
| 185ba0f50a | |||
| 49cabaa1bc | |||
| 11b6d5254e | |||
| aa5d59ba5b |
@@ -0,0 +1,41 @@
|
||||
# Irrelevant source files
|
||||
deployment/
|
||||
|
||||
# Build output and other temporary files
|
||||
.husky/_/
|
||||
.netlify/
|
||||
.nuxt/
|
||||
dist/
|
||||
node_modules/
|
||||
|
||||
# Version control
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# GitHub / CI metadata
|
||||
.github/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
*.env
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# IDE / editor settings
|
||||
.vscode/
|
||||
.idea/
|
||||
.zed/
|
||||
*.iml
|
||||
|
||||
# OS / editor junk
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# AI
|
||||
.claude/
|
||||
.cursor/
|
||||
|
||||
# testing
|
||||
tests/
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Build Docker Container
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
PUBLISH:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
IMAGE_VERSION_TAG:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
name: Build Docker image
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to be able to push images to ghcr.io. As permissions is static, it has to be granted even if PUBLISH is false
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Login to Helm Chart & Container Image Registry
|
||||
if: ${{ inputs.PUBLISH == true }}
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
- name: Setup Docker Builder
|
||||
uses: useblacksmith/setup-docker-builder@affa10db466676f3dfb3e54caeb228ee0691510f
|
||||
- name: Build and push
|
||||
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1
|
||||
with:
|
||||
push: ${{ inputs.PUBLISH }}
|
||||
tags: ghcr.io/specklesystems/speckle-dui:${{ inputs.IMAGE_VERSION_TAG }}
|
||||
file: ./deployment/docker/Dockerfile
|
||||
network: host # to be able to connect to Tailscale and pull private base image during build
|
||||
allow: network.host # to be able to connect to Tailscale and pull private base image during build
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Get Version
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
IMAGE_VERSION_TAG:
|
||||
description: 'The image version tag under which the Helm chart and docker image should be published'
|
||||
value: ${{ jobs.get-version.outputs.VERSION }}
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-get-version-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
outputs:
|
||||
VERSION: ${{ steps.get-version.outputs.VERSION }}
|
||||
name: Get Version
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
sparse-checkout: ''
|
||||
fetch-depth: 1
|
||||
fetch-tags: 1
|
||||
persist-credentials: true # zizmor: ignore[artipacked] need to fetch tags in the next step and this ensures that git is configured & authenticated
|
||||
- run: git fetch origin 'refs/tags/*:refs/tags/*'
|
||||
- name: Get version tag
|
||||
id: get-version
|
||||
run: |
|
||||
VERSION=""
|
||||
if [[ "${GITHUB_REF_NAME}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "${VERSION} is a valid semver, we shall use it. Exiting"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_RELEASE="$(git describe --always --tags $(git rev-list --tags --max-count=1) | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" # get the last release tag. FIXME: Fails if a commit is tagged with more than one tag: https://stackoverflow.com/questions/8089002/git-describe-with-two-tags-on-the-same-commit/56039163#56039163
|
||||
LAST_RELEASE="${LAST_RELEASE:-0.0.0}"
|
||||
NEXT_RELEASE="$(echo "${LAST_RELEASE}" | awk -F. -v OFS=. '{$NF += 1 ; print}')"
|
||||
|
||||
if [[ "${GITHUB_REF_NAME}" == "main" ]]; then
|
||||
VERSION="${NEXT_RELEASE}-alpha.${GITHUB_RUN_NUMBER}"
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "${VERSION} will be an alpha version. Exiting"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BRANCH_NAME_TRUNCATED="$(echo "${GITHUB_REF_NAME}" | cut -c -28 | sed 's/[^a-zA-Z0-9.-]/-/g')" # docker has a 128 character tag limit, so ensuring the branch name will be short enough
|
||||
|
||||
PADDED_RUN_NUMBER="$(printf "%06d" "${GITHUB_RUN_NUMBER}")"
|
||||
COMMIT_SHA1_TRUNCATED="$(echo "${GITHUB_SHA}" | cut -c -7)"
|
||||
|
||||
VERSION="${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}.${PADDED_RUN_NUMBER}-${COMMIT_SHA1_TRUNCATED}"
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "${VERSION} will be a branch build version. Exiting"
|
||||
exit 0
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
workflow_call: {}
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-lint-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f
|
||||
with:
|
||||
node-version: '22.14.0'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run Linter
|
||||
run: yarn lint
|
||||
+30
-35
@@ -1,44 +1,39 @@
|
||||
name: Linting
|
||||
name: Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true # other running workflows get cancelled on the same branch
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
get-version:
|
||||
uses: ./.github/workflows/get-version.yml
|
||||
with: {}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
lint:
|
||||
uses: ./.github/workflows/lint.yml
|
||||
with: {}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
- 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
|
||||
build:
|
||||
needs:
|
||||
- get-version
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
PUBLISH: false
|
||||
IMAGE_VERSION_TAG: ${{ needs.get-version.outputs.IMAGE_VERSION_TAG }}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to be able to push images to ghcr.io, even if PUBLISH is false, as permissions is static at workflow level
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true # other running workflows get cancelled on the same branch
|
||||
|
||||
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
uses: ./.github/workflows/get-version.yml
|
||||
with: {}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
lint:
|
||||
uses: ./.github/workflows/lint.yml
|
||||
with: {}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
needs:
|
||||
- get-version
|
||||
- lint
|
||||
with:
|
||||
PUBLISH: true
|
||||
IMAGE_VERSION_TAG: ${{ needs.get-version.outputs.IMAGE_VERSION_TAG }}
|
||||
secrets: {}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # to be able to push images to ghcr.io
|
||||
+3
-1
@@ -15,4 +15,6 @@ dist
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
!.yarn/versions
|
||||
|
||||
.claude
|
||||
|
||||
+5
-1
@@ -32,4 +32,8 @@ venv
|
||||
|
||||
storybook-static
|
||||
.tshy
|
||||
.tshy-build
|
||||
.tshy-build
|
||||
|
||||
# Helm
|
||||
deployment/helm
|
||||
tests/deployment
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div v-if="!hidden" class="flex flex-col space-y-2">
|
||||
<!-- idle: server URL + sign in button -->
|
||||
<template v-if="state === 'idle'">
|
||||
<div class="flex space-x-2">
|
||||
<FormButton
|
||||
v-if="canAddAccount"
|
||||
full-width
|
||||
color="outline"
|
||||
@click="openBrowserAuth()"
|
||||
>
|
||||
Log in with OAuth token
|
||||
</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- waiting: instructions + code input -->
|
||||
<template v-if="state === 'waiting' || state === 'submitting'">
|
||||
<div class="text-foreground-2 space-y-2 border rounded-lg p-2">
|
||||
<div class="text-sm text-center">
|
||||
Check your browser: authorize the app, then copy the exchange code and paste
|
||||
it below.
|
||||
</div>
|
||||
<div class="py-2"><CommonLoadingBar :loading="state === 'waiting'" /></div>
|
||||
<FormTextInput
|
||||
v-model="exchangeCode"
|
||||
name="exchangeCode"
|
||||
:show-label="false"
|
||||
placeholder="Paste exchange code here"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
:disabled="state === 'submitting'"
|
||||
/>
|
||||
<FormButton
|
||||
full-width
|
||||
:disabled="!exchangeCode?.trim() || state === 'submitting'"
|
||||
@click="submitCode()"
|
||||
>
|
||||
{{ state === 'submitting' ? 'Signing in...' : 'Submit' }}
|
||||
</FormButton>
|
||||
|
||||
<div v-if="showHelp" class="p-2 rounded-md space-y-1">
|
||||
<div class="text-sm text-center">Having trouble?</div>
|
||||
<div class="flex justify-center">
|
||||
<span>
|
||||
<FormButton size="sm" text @click="retryFlow()">Retry</FormButton>
|
||||
or
|
||||
<FormButton text size="sm" @click="$openUrl('https://speckle.community')">
|
||||
Get in touch with us
|
||||
</FormButton>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- error -->
|
||||
<template v-if="state === 'error'">
|
||||
<div class="text-foreground-2 space-y-2">
|
||||
<div class="text-sm text-center text-red-500">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<FormButton full-width @click="retryFlow()">Try again</FormButton>
|
||||
<FormButton text size="sm" full-width @click="emit('backToSignIn')">
|
||||
Back
|
||||
</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAuthManager } from '~/lib/authn/useAuthManager'
|
||||
import { useTokenExchange, supportsOAuthToken } from '~/lib/authn/useTokenExchange'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import type { BaseBridge } from '~/lib/bridge/base'
|
||||
|
||||
const props = defineProps<{
|
||||
serverUrl: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'backToSignIn'): void
|
||||
}>()
|
||||
|
||||
const app = useNuxtApp()
|
||||
const { generateLocalChallenge } = useAuthManager()
|
||||
const { exchangeAccessCode } = useTokenExchange()
|
||||
const { trackEvent } = useMixpanel()
|
||||
const accountStore = useAccountStore()
|
||||
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
|
||||
const state = ref<'idle' | 'waiting' | 'submitting' | 'error'>('idle')
|
||||
const exchangeCode = ref<string | undefined>()
|
||||
const errorMessage = ref('')
|
||||
const showHelp = ref(false)
|
||||
const hidden = ref(false)
|
||||
|
||||
const checkServerSupport = async (url: string) => {
|
||||
const serverUrl = url ? new URL(url).origin : 'https://app.speckle.systems'
|
||||
hidden.value = !(await supportsOAuthToken(serverUrl))
|
||||
}
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
onMounted(() => checkServerSupport(props.serverUrl))
|
||||
watch(
|
||||
() => props.serverUrl,
|
||||
(url) => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => checkServerSupport(url), 500)
|
||||
}
|
||||
)
|
||||
|
||||
let currentCodeVerifier = ''
|
||||
let currentCodeChallenge = ''
|
||||
let currentServerUrl = ''
|
||||
|
||||
const openBrowserAuth = async () => {
|
||||
currentServerUrl = props.serverUrl
|
||||
? new URL(props.serverUrl).origin
|
||||
: 'https://app.speckle.systems'
|
||||
|
||||
const { codeVerifier, codeChallenge } = await generateLocalChallenge()
|
||||
currentCodeVerifier = codeVerifier
|
||||
currentCodeChallenge = codeChallenge
|
||||
const authUrl = `${currentServerUrl}/authn/verify/sdui/${codeChallenge}?returnExchangeToken=true&code_challenge_method=S256`
|
||||
app.$openUrl(authUrl)
|
||||
|
||||
state.value = 'waiting'
|
||||
exchangeCode.value = undefined
|
||||
showHelp.value = false
|
||||
|
||||
setTimeout(() => {
|
||||
if (state.value === 'waiting') {
|
||||
showHelp.value = true
|
||||
}
|
||||
}, 10_000)
|
||||
}
|
||||
|
||||
const submitCode = async () => {
|
||||
const code = exchangeCode.value?.trim()
|
||||
if (!code || !currentCodeChallenge || !currentServerUrl) return
|
||||
|
||||
state.value = 'submitting'
|
||||
try {
|
||||
await exchangeAccessCode(
|
||||
currentServerUrl,
|
||||
code,
|
||||
currentCodeChallenge,
|
||||
currentCodeVerifier
|
||||
)
|
||||
void trackEvent('DUI Account Added')
|
||||
// Refresh accounts so the watcher in Menu.vue detects the new account and closes the dialog
|
||||
await accountStore.refreshAccounts()
|
||||
} catch (error) {
|
||||
errorMessage.value =
|
||||
error instanceof Error ? error.message : 'Failed to sign in. Please try again.'
|
||||
state.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
const retryFlow = () => {
|
||||
state.value = 'idle'
|
||||
exchangeCode.value = undefined
|
||||
errorMessage.value = ''
|
||||
showHelp.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div v-if="isDesktopServiceAvailable">
|
||||
<div v-show="!isAddingAccount" class="text-foreground-2 space-y-2">
|
||||
<div class="flex space-x-2">
|
||||
<FormButton full-width color="outline" @click="startAccountAddFlow()">
|
||||
Log in (Legacy)
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="isAddingAccount"
|
||||
class="text-foreground-2 mt-2 mb-4 space-y-2 border rounded-lg p-2"
|
||||
>
|
||||
<div class="text-sm text-center">
|
||||
Please check your browser: waiting for authorization to complete.
|
||||
</div>
|
||||
<div class="py-2"><CommonLoadingBar :loading="isAddingAccount" /></div>
|
||||
<div v-if="showHelp" class="p-2 rounded-md space-y-1">
|
||||
<div class="text-sm text-center">Having trouble?</div>
|
||||
<div class="flex justify-center">
|
||||
<span>
|
||||
<FormButton text size="sm" @click="$openUrl('https://speckle.community')">
|
||||
Get in touch with us
|
||||
</FormButton>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<div class="text-foreground-2 text-sm">
|
||||
The Speckle Desktop Service is required to add accounts as legacy way. This
|
||||
background service handles authentication securely.
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<FormButton
|
||||
color="outline"
|
||||
class="px-1"
|
||||
:icon-left="ArrowLeftIcon"
|
||||
hide-text
|
||||
@click="emit('backToSignIn')"
|
||||
/>
|
||||
<FormButton full-width @click="$openUrl('https://releases.speckle.systems')">
|
||||
Download Desktop Service
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
import { ToastNotificationType } from '@speckle/ui-components'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { useAccountStore } from '~~/store/accounts'
|
||||
import { useDesktopService } from '~/lib/core/composables/desktopService'
|
||||
import { ArrowLeftIcon } from '@heroicons/vue/24/solid'
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const { pingDesktopService } = useDesktopService()
|
||||
const hostApp = useHostAppStore()
|
||||
const app = useNuxtApp()
|
||||
const { trackEvent } = useMixpanel()
|
||||
|
||||
const props = defineProps<{
|
||||
serverUrl: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'backToSignIn'): void
|
||||
}>()
|
||||
|
||||
const showCustomServerInput = ref(false)
|
||||
const isAddingAccount = ref(false)
|
||||
const isDesktopServiceAvailable = ref(false) // this should be false default because there is a delay if /ping is not successful.
|
||||
const showHelp = ref(false)
|
||||
|
||||
const accountCheckerIntervalFn = useIntervalFn(
|
||||
async () => {
|
||||
const previousAccountCount = accountStore.accounts.length
|
||||
await accountStore.refreshAccounts()
|
||||
const currentAccountCount = accountStore.accounts.length
|
||||
if (previousAccountCount !== currentAccountCount) {
|
||||
isAddingAccount.value = false
|
||||
showCustomServerInput.value = false
|
||||
accountCheckerIntervalFn.pause()
|
||||
trackEvent('DUI Account Added')
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
const startAccountAddFlow = () => {
|
||||
isAddingAccount.value = true
|
||||
accountCheckerIntervalFn.resume()
|
||||
setTimeout(() => {
|
||||
showHelp.value = true
|
||||
}, 10_000)
|
||||
const url = props.serverUrl
|
||||
? `http://localhost:29364/auth/add-account?serverUrl=${
|
||||
new URL(props.serverUrl).origin
|
||||
}`
|
||||
: `http://localhost:29364/auth/add-account`
|
||||
|
||||
app.$openUrl(url)
|
||||
|
||||
// this is a annoying timeout that we cannot detect if user added same account or not.
|
||||
setTimeout(() => {
|
||||
if (isAddingAccount.value) {
|
||||
isAddingAccount.value = false
|
||||
showCustomServerInput.value = false
|
||||
accountCheckerIntervalFn.pause()
|
||||
// Note to Dim: not sure about toast
|
||||
hostApp.setNotification({
|
||||
title: 'Sign In',
|
||||
type: ToastNotificationType.Info,
|
||||
description:
|
||||
'Sign in timed out. This may have happened because you tried adding an existing account.'
|
||||
})
|
||||
// TODO: we could log it to sentry/seq later to see how likely it happens?
|
||||
}
|
||||
}, 30_000)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isDesktopServiceAvailable.value = await pingDesktopService()
|
||||
})
|
||||
</script>
|
||||
@@ -39,17 +39,23 @@
|
||||
title="Add a new account"
|
||||
fullscreen="none"
|
||||
>
|
||||
<div>
|
||||
<div v-if="isDesktopServiceAvailable">
|
||||
<AccountsSignInFlow />
|
||||
</div>
|
||||
<div v-else class="flex flex-wrap justify-center space-x-4 max-width">
|
||||
<FormButton text @click="$openUrl(`speckle://accounts`)">
|
||||
Add account via Manager
|
||||
</FormButton>
|
||||
<FormButton text @click="accountStore.refreshAccounts()">
|
||||
Refresh accounts
|
||||
</FormButton>
|
||||
<div class="flex flex-col space-y-4 p-2">
|
||||
<FormTextInput
|
||||
v-model="customServerUrl"
|
||||
name="Server to sign in"
|
||||
show-label
|
||||
placeholder="https://app.speckle.systems"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
show-clear
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<AccountsSignInFlow :server-url="customServerUrl" />
|
||||
<AccountsExchangeTokenSignInFlow :server-url="customServerUrl" />
|
||||
<AccountsLegacySignInFlow
|
||||
v-if="!canStartAuthAccount"
|
||||
:server-url="customServerUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CommonDialog>
|
||||
@@ -58,6 +64,7 @@
|
||||
</CommonDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { XMarkIcon } from '@heroicons/vue/20/solid'
|
||||
@@ -65,11 +72,18 @@ import type { DUIAccount } from '~/store/accounts'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { useDesktopService } from '~/lib/core/composables/desktopService'
|
||||
import type { BaseBridge } from '~/lib/bridge/base'
|
||||
|
||||
const { trackEvent } = useMixpanel()
|
||||
const app = useNuxtApp()
|
||||
const { $openUrl } = useNuxtApp()
|
||||
const { pingDesktopService } = useDesktopService()
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const canStartAuthAccount = ['AuthenticateAccount', 'authenticateAccount'].some(
|
||||
(name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
|
||||
const customServerUrl = ref<string>('https://app.speckle.systems')
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -86,7 +100,7 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const showAddNewAccount = ref(false)
|
||||
// const showAccountsDialog = ref(false)
|
||||
const signInMode = ref<'default' | 'exchange' | 'legacy'>('default')
|
||||
|
||||
const showAccountsDialog = defineModel<boolean>('open', {
|
||||
required: false,
|
||||
@@ -106,6 +120,13 @@ watch(showAccountsDialog, (newVal) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(showAddNewAccount, (newVal) => {
|
||||
if (newVal) {
|
||||
// reset the sign-in mode on every add account sub-dialog
|
||||
signInMode.value = 'default'
|
||||
}
|
||||
})
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const { accounts, activeAccount, userSelectedAccount, isLoading } =
|
||||
storeToRefs(accountStore)
|
||||
@@ -144,7 +165,6 @@ const user = computed(() => {
|
||||
// acc = currentSelectedAccount
|
||||
// }
|
||||
// }
|
||||
|
||||
return {
|
||||
name: activeAccount.value.accountInfo.userInfo.name,
|
||||
avatar: activeAccount.value.accountInfo.userInfo.avatar
|
||||
|
||||
@@ -1,118 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-show="!isAddingAccount" class="text-foreground-2 my-2 space-y-2">
|
||||
<div v-if="showCustomServerInput">
|
||||
<FormTextInput
|
||||
v-model="customServerUrl"
|
||||
name="name"
|
||||
:show-label="false"
|
||||
placeholder="https://app.speckle.systems"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
show-clear
|
||||
@clear="showCustomServerInput = false"
|
||||
/>
|
||||
</div>
|
||||
<FormButton full-width @click="startAccountAddFlow()">Sign In</FormButton>
|
||||
<FormButton
|
||||
text
|
||||
size="sm"
|
||||
full-width
|
||||
@click="showCustomServerInput = !showCustomServerInput"
|
||||
>
|
||||
{{ showCustomServerInput ? 'Use default server' : 'Set custom server url' }}
|
||||
</FormButton>
|
||||
</div>
|
||||
|
||||
<div v-show="isAddingAccount" class="text-foreground-2 mt-2 mb-4 space-y-2">
|
||||
<div class="text-sm text-center">
|
||||
Please check your browser: waiting for authorization to complete.
|
||||
</div>
|
||||
<div class="py-2"><CommonLoadingBar :loading="isAddingAccount" /></div>
|
||||
<div v-if="showHelp" class="bg-blue-500/10 p-2 rounded-md space-y-2">
|
||||
<div class="text-sm text-center">Having trouble?</div>
|
||||
<FormButton size="sm" full-width @click="restartFlow()">Retry</FormButton>
|
||||
<FormButton
|
||||
text
|
||||
size="sm"
|
||||
full-width
|
||||
@click="$openUrl('https://speckle.community')"
|
||||
>
|
||||
Get in touch with us
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<FormButton v-if="canAddAccount" full-width @click="logIn()">Log in</FormButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { useAccountStore } from '~~/store/accounts'
|
||||
import { useAuthManager } from '~/lib/authn/useAuthManager'
|
||||
import type { BaseBridge } from '~/lib/bridge/base'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
import { ToastNotificationType } from '@speckle/ui-components'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
|
||||
const props = defineProps<{
|
||||
serverUrl: string
|
||||
}>()
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const hostApp = useHostAppStore()
|
||||
const app = useNuxtApp()
|
||||
const { trackEvent } = useMixpanel()
|
||||
|
||||
const customServerUrl = ref<string | undefined>(undefined)
|
||||
const isAddingAccount = ref(false)
|
||||
const showHelp = ref(false)
|
||||
const showCustomServerInput = ref(false)
|
||||
|
||||
const accountCheckerIntervalFn = useIntervalFn(
|
||||
async () => {
|
||||
const previousAccountCount = accountStore.accounts.length
|
||||
await accountStore.refreshAccounts()
|
||||
const currentAccountCount = accountStore.accounts.length
|
||||
if (previousAccountCount !== currentAccountCount) {
|
||||
isAddingAccount.value = false
|
||||
showCustomServerInput.value = false
|
||||
accountCheckerIntervalFn.pause()
|
||||
trackEvent('DUI Account Added')
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{ immediate: false }
|
||||
const hostAppStore = useHostAppStore()
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
const canStartAuthAccount = ['AuthenticateAccount', 'authenticateAccount'].some(
|
||||
(name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
|
||||
const startAccountAddFlow = () => {
|
||||
isAddingAccount.value = true
|
||||
accountCheckerIntervalFn.resume()
|
||||
setTimeout(() => {
|
||||
showHelp.value = true
|
||||
}, 10_000)
|
||||
const url = customServerUrl.value
|
||||
? `http://localhost:29364/auth/add-account?serverUrl=${
|
||||
new URL(customServerUrl.value).origin
|
||||
}`
|
||||
: `http://localhost:29364/auth/add-account`
|
||||
const { generateChallenge } = useAuthManager()
|
||||
|
||||
app.$openUrl(url)
|
||||
|
||||
// this is a annoying timeout that we cannot detect if user added same account or not.
|
||||
setTimeout(() => {
|
||||
if (isAddingAccount.value) {
|
||||
isAddingAccount.value = false
|
||||
showCustomServerInput.value = false
|
||||
accountCheckerIntervalFn.pause()
|
||||
// Note to Dim: not sure about toast
|
||||
hostApp.setNotification({
|
||||
title: 'Sign In',
|
||||
const logIn = async () => {
|
||||
const serverUrl = props.serverUrl
|
||||
? new URL(props.serverUrl).origin
|
||||
: 'https://app.speckle.systems'
|
||||
if (canStartAuthAccount) {
|
||||
const acc = await $accountBinding.authenticateAccount(serverUrl)
|
||||
if (acc.token) {
|
||||
await accountStore.refreshAccounts()
|
||||
} else {
|
||||
hostAppStore.setNotification({
|
||||
title: 'Log In',
|
||||
type: ToastNotificationType.Info,
|
||||
description:
|
||||
'Sign in timed out. This may have happened because you tried adding an existing account.'
|
||||
"Log in could not completed. Make sure you have logged in successfully, otherwise try 'Log in with OAuth token'"
|
||||
})
|
||||
// TODO: we could log it to sentry/seq later to see how likely it happens?
|
||||
}
|
||||
}, 30_000)
|
||||
}
|
||||
|
||||
const restartFlow = () => {
|
||||
isAddingAccount.value = false
|
||||
showHelp.value = false
|
||||
} else {
|
||||
const { codeChallenge } = await generateChallenge(serverUrl)
|
||||
const authUrl = `${serverUrl}/authn/verify/sdui/${codeChallenge}?code_challenge_method=S256`
|
||||
window.location.href = authUrl
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
<!-- NOT WILL BE USED SINCE WE ENABLE AUTOMATION CREATION FROM DUI3 -->
|
||||
<template>
|
||||
<div class="p-0">
|
||||
<slot name="activator" :toggle="toggleDialog"></slot>
|
||||
<CommonDialog
|
||||
v-model:open="showAutomateDialog"
|
||||
:title="`Settings`"
|
||||
fullscreen="none"
|
||||
>
|
||||
<div v-if="hasFunctions">
|
||||
<FormSelectBase
|
||||
key="name"
|
||||
v-model="selectedFunction"
|
||||
clearable
|
||||
label="Automate functions"
|
||||
placeholder="Nothing selected"
|
||||
name="Functions"
|
||||
show-label
|
||||
:items="functions"
|
||||
mount-menu-on-body
|
||||
>
|
||||
<template #something-selected="{ value }">
|
||||
<span>{{ isArray(value) ? value[0].name : value.name }}</span>
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
<div class="flex items-center">
|
||||
<span class="truncate">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
</div>
|
||||
<div v-if="selectedFunction && finalParams && step === 0">
|
||||
<FormJsonForm
|
||||
ref="jsonForm"
|
||||
:data="data"
|
||||
:schema="finalParams"
|
||||
class="space-y-4"
|
||||
:validate-on-mount="false"
|
||||
@change="handler"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="step === 1">
|
||||
<FormTextInput
|
||||
v-model="automationName"
|
||||
name="automationName"
|
||||
label="Automation name"
|
||||
color="foundation"
|
||||
show-label
|
||||
help="Give your automation a name"
|
||||
placeholder="Name"
|
||||
show-required
|
||||
validate-on-value-update
|
||||
/>
|
||||
</div>
|
||||
<FormButton
|
||||
v-if="selectedFunction && step === 0"
|
||||
size="sm"
|
||||
class="mt-4"
|
||||
@click="step++"
|
||||
>
|
||||
Next
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="selectedFunction && step === 1"
|
||||
size="sm"
|
||||
class="mt-4"
|
||||
@click="createAutomationHandler"
|
||||
>
|
||||
Create
|
||||
</FormButton>
|
||||
</CommonDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { AutomateFunctionItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
automateFunctionsQuery,
|
||||
createAutomationMutation
|
||||
} from '~/lib/graphql/mutationsAndQueries'
|
||||
import { provideApolloClient, useMutation, useQuery } from '@vue/apollo-composable'
|
||||
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
|
||||
modelId: string
|
||||
}>()
|
||||
|
||||
const step = ref<number>(0)
|
||||
|
||||
const automationName = ref<string>('')
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const { activeAccount } = storeToRefs(accountStore)
|
||||
const accountId = computed(() => activeAccount.value?.accountInfo.id) // NOTE: none of the tokens here has read, write access to automate, only frontend tokens have. Keep in mind after first pass!
|
||||
|
||||
const selectedFunction = ref<AutomateFunctionItemFragment>()
|
||||
|
||||
const showAutomateDialog = ref(false)
|
||||
|
||||
const toggleDialog = () => {
|
||||
showAutomateDialog.value = !showAutomateDialog.value
|
||||
}
|
||||
|
||||
const { mutate } = provideApolloClient((activeAccount.value as DUIAccount).client)(() =>
|
||||
useMutation(createAutomationMutation)
|
||||
)
|
||||
|
||||
const createAutomationHandler = async () => {
|
||||
const _res = await mutate({
|
||||
projectId: props.projectId,
|
||||
input: { name: automationName.value, enabled: false }
|
||||
})
|
||||
showAutomateDialog.value = false
|
||||
}
|
||||
|
||||
const { result: functionsResult, onError } = useQuery(
|
||||
automateFunctionsQuery,
|
||||
() => ({}),
|
||||
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
|
||||
)
|
||||
|
||||
onError((err: ApolloError) => {
|
||||
console.warn(err.message)
|
||||
})
|
||||
|
||||
const functions = computed(() => functionsResult.value?.automateFunctions.items)
|
||||
const hasFunctions = computed(() => functions.value?.length !== 0)
|
||||
|
||||
const release = computed(() =>
|
||||
selectedFunction.value?.releases.items.length
|
||||
? selectedFunction.value?.releases.items[0]
|
||||
: undefined
|
||||
)
|
||||
|
||||
const finalParams = computed(() => formatVersionParams(release.value?.inputSchema))
|
||||
|
||||
const { handler } = useJsonFormsChangeHandler({
|
||||
schema: finalParams
|
||||
})
|
||||
|
||||
console.log(finalParams)
|
||||
|
||||
type DataType = Record<string, unknown>
|
||||
const data = computed(() => {
|
||||
const kvp = {} as DataType
|
||||
if (finalParams.value) {
|
||||
Object.entries(finalParams.value).forEach((k, _) => {
|
||||
kvp[k as unknown as string] = undefined
|
||||
})
|
||||
}
|
||||
return kvp
|
||||
})
|
||||
</script>
|
||||
@@ -27,16 +27,17 @@
|
||||
>
|
||||
{{ notification.secondaryCta.name }}
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="notification.cta"
|
||||
v-tippy="notification.cta.tooltipText"
|
||||
size="sm"
|
||||
color="primary"
|
||||
full-width
|
||||
@click.stop="notification.cta?.action"
|
||||
>
|
||||
{{ notification.cta.name }}
|
||||
</FormButton>
|
||||
<div v-if="notification.cta" v-tippy="notification.cta.tooltipText">
|
||||
<FormButton
|
||||
:disabled="notification.cta.disabled"
|
||||
size="sm"
|
||||
color="primary"
|
||||
full-width
|
||||
@click.stop="notification.cta?.action"
|
||||
>
|
||||
{{ notification.cta.name }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<CommonAlert
|
||||
v-if="
|
||||
store.isDistributedBySpeckle &&
|
||||
store.latestAvailableVersion &&
|
||||
!store.isConnectorUpToDate &&
|
||||
!hasDismissedAlert &&
|
||||
!store.isUpdateNotificationDisabled
|
||||
|
||||
@@ -66,6 +66,13 @@
|
||||
@update:filter="(filter : ISendFilter) => (selectedFilter = filter)"
|
||||
/>
|
||||
</div>
|
||||
<!-- I dont like the way we use revit categories filter for archicad layers, this component need to be generalized if we have one more -->
|
||||
<div v-else-if="selectedFilter.id === 'archicadLayers'">
|
||||
<FilterRevitCategories
|
||||
:filter="(selectedFilter as RevitCategoriesSendFilter)"
|
||||
@update:filter="(filter : ISendFilter) => (selectedFilter = filter)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Below should have been implemented as sendFilterSelect as above, we can delete it later -->
|
||||
<div v-else-if="selectedFilter.id === 'navisworksSavedSets'">
|
||||
<FilterFormSelect
|
||||
|
||||
@@ -3,49 +3,61 @@
|
||||
<div class="text-foreground-2 text-body-2xs mb-1 pl-1">
|
||||
{{ control.label }}
|
||||
</div>
|
||||
<FormSelectMulti
|
||||
:model-value="modelValue"
|
||||
:name="fieldName"
|
||||
:rules="multiValidator"
|
||||
:label="control.label"
|
||||
:items="control.options"
|
||||
clearable
|
||||
:search="true"
|
||||
:search-placeholder="'Search'"
|
||||
:filter-predicate="searchFilterPredicate"
|
||||
:help="control.description"
|
||||
:allow-unset="false"
|
||||
by="value"
|
||||
button-style="tinted"
|
||||
:validate-on-value-update="validateOnValueUpdate"
|
||||
mount-menu-on-body
|
||||
@update:model-value="handleChange"
|
||||
>
|
||||
<template #nothing-selected>
|
||||
{{
|
||||
appliedOptions['placeholder']
|
||||
? appliedOptions['placeholder']
|
||||
: 'Select values'
|
||||
}}
|
||||
</template>
|
||||
<template #something-selected="{ value }">
|
||||
<div ref="elementToWatchForChanges" class="flex items-center space-x-0.5">
|
||||
<div ref="itemContainer" class="flex flex-wrap overflow-hidden space-x-0.5">
|
||||
<div v-for="(item, i) in value" :key="item.value" class="text-foreground">
|
||||
{{ item.label + (i < value.length - 1 ? ', ' : '') }}
|
||||
<!-- button next to component (like revit send categories) -->
|
||||
<!-- min width to keep components "in-sync" at narrow sizes -->
|
||||
<!-- size "sm" matches height of select all toggle -->
|
||||
<div class="flex items-center space-x-2 min-w-72">
|
||||
<FormSelectMulti
|
||||
:model-value="modelValue"
|
||||
:name="fieldName"
|
||||
:rules="multiValidator"
|
||||
:label="control.label"
|
||||
:items="control.options"
|
||||
class="flex-1 min-w-0"
|
||||
clearable
|
||||
:search="true"
|
||||
:search-placeholder="'Search'"
|
||||
:filter-predicate="searchFilterPredicate"
|
||||
:help="control.description"
|
||||
:allow-unset="false"
|
||||
by="value"
|
||||
button-style="tinted"
|
||||
:validate-on-value-update="validateOnValueUpdate"
|
||||
mount-menu-on-body
|
||||
fixed-height
|
||||
@update:model-value="handleChange"
|
||||
>
|
||||
<template #nothing-selected>
|
||||
{{
|
||||
appliedOptions['placeholder']
|
||||
? appliedOptions['placeholder']
|
||||
: 'Select values'
|
||||
}}
|
||||
</template>
|
||||
<template #something-selected="{ value }">
|
||||
<div ref="elementToWatchForChanges" class="flex items-center space-x-0.5">
|
||||
<div ref="itemContainer" class="flex flex-wrap overflow-hidden space-x-0.5">
|
||||
<div v-for="(item, i) in value" :key="item.value" class="text-foreground">
|
||||
{{ item.label + (i < value.length - 1 ? ', ' : '') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hiddenSelectedItemCount > 0" class="text-foreground-2 normal">
|
||||
+{{ hiddenSelectedItemCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hiddenSelectedItemCount > 0" class="text-foreground-2 normal">
|
||||
+{{ hiddenSelectedItemCount }}
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
<div class="flex items-center text-foreground-2 text-body-2xs">
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
<div class="flex items-center text-foreground-2 text-body-2xs">
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</FormSelectMulti>
|
||||
</template>
|
||||
</FormSelectMulti>
|
||||
|
||||
<!-- Select All / Deselect All button - positioned next to dropdown like Revit -->
|
||||
<FormButton color="outline" class="min-w-28" size="base" @click="toggleSelectAll">
|
||||
{{ allSelected ? 'Deselect all' : 'Select all' }}
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -108,4 +120,27 @@ const modelValue = computed(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
return control.value.options.filter((o) => val?.includes(o.value))
|
||||
})
|
||||
|
||||
/**
|
||||
* Computed property to check if all available options are selected.
|
||||
*/
|
||||
const allSelected = computed(() => {
|
||||
const currentSelection = modelValue.value || []
|
||||
const allOptions = control.value.options || []
|
||||
return currentSelection.length === allOptions.length && allOptions.length > 0
|
||||
})
|
||||
|
||||
/**
|
||||
* Toggle between selecting all categories and clearing all selections.
|
||||
*/
|
||||
const toggleSelectAll = () => {
|
||||
if (allSelected.value) {
|
||||
// deselect all -> pass empty array
|
||||
handleChange([])
|
||||
} else {
|
||||
// select all available options
|
||||
const allOptions = control.value.options || []
|
||||
handleChange(allOptions)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.73336 1.45469C7.57004 1.29277 8.43001 1.29277 9.26669 1.45469M9.26669 14.5454C8.43001 14.7073 7.57004 14.7073 6.73336 14.5454M11.7394 2.48069C12.447 2.96017 13.0558 3.57127 13.5327 4.28069M1.45469 9.26669C1.29277 8.43001 1.29277 7.57004 1.45469 6.73336M13.5194 11.7394C13.0399 12.447 12.4288 13.0558 11.7194 13.5327M14.5454 6.73336C14.7073 7.57004 14.7073 8.43001 14.5454 9.26669M2.48069 4.26069C2.96017 3.55304 3.57127 2.94421 4.28069 2.46736M4.26069 13.5194C3.55304 13.0399 2.94421 12.4288 2.46736 11.7194"
|
||||
stroke="#707070"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 1.33301C11.6819 1.33301 14.667 4.3181 14.667 8C14.6669 11.6819 11.6819 14.667 8 14.667C4.3182 14.6669 1.33305 11.6818 1.33301 8C1.33301 4.31816 4.31818 1.3331 8 1.33301ZM10.5303 6.13672C10.2374 5.84383 9.76262 5.84383 9.46973 6.13672L7.33301 8.27246L6.53027 7.46973C6.23742 7.17705 5.76258 7.17705 5.46973 7.46973C5.17713 7.76259 5.17708 8.23745 5.46973 8.53027L6.80273 9.86426C6.94329 10.0047 7.13433 10.0839 7.33301 10.084C7.53165 10.084 7.72268 10.0046 7.86328 9.86426L10.5303 7.19727C10.8231 6.90445 10.8229 6.42963 10.5303 6.13672Z"
|
||||
fill="#15803D"
|
||||
/>
|
||||
<path
|
||||
d="M8 1.33301C11.6819 1.33301 14.667 4.3181 14.667 8C14.6669 11.6819 11.6819 14.667 8 14.667C4.3182 14.6669 1.33305 11.6818 1.33301 8C1.33301 4.31816 4.31818 1.3331 8 1.33301ZM10.5303 6.13672C10.2374 5.84383 9.76262 5.84383 9.46973 6.13672L7.33301 8.27246L6.53027 7.46973C6.23742 7.17705 5.76258 7.17705 5.46973 7.46973C5.17713 7.76259 5.17708 8.23745 5.46973 8.53027L6.80273 9.86426C6.94329 10.0047 7.13433 10.0839 7.33301 10.084C7.53165 10.084 7.72268 10.0046 7.86328 9.86426L10.5303 7.19727C10.8231 6.90445 10.8229 6.42963 10.5303 6.13672Z"
|
||||
fill="#16A34A"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.00024 2.08337C11.2678 2.08355 13.9163 4.73279 13.9163 8.00037C13.9161 11.2678 11.2677 13.9162 8.00024 13.9164C4.73267 13.9164 2.08343 11.2679 2.08325 8.00037C2.08325 4.73268 4.73256 2.08337 8.00024 2.08337Z"
|
||||
stroke="#EAB308"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8.75 4.83789C10.1832 5.17655 11.25 6.46328 11.25 8C11.25 9.53664 10.1831 10.8224 8.75 11.1611V4.83789Z"
|
||||
fill="#EAB308"
|
||||
/>
|
||||
<path
|
||||
d="M8.75 4.83789C10.1832 5.17655 11.25 6.46328 11.25 8C11.25 9.53664 10.1831 10.8224 8.75 11.1611V4.83789Z"
|
||||
stroke="#7C7C7D"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8.75 4.83789C10.1832 5.17655 11.25 6.46328 11.25 8C11.25 9.53664 10.1831 10.8224 8.75 11.1611V4.83789Z"
|
||||
stroke="#EAB308"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -49,9 +49,26 @@
|
||||
>
|
||||
<span class="">Update</span>
|
||||
</FormButton> -->
|
||||
<div class="text-[8px] text-foreground-disabled max-[150px]:hidden">
|
||||
|
||||
<div
|
||||
class="text-[8px] text-foreground-disabled max-[150px]:hidden"
|
||||
:class="{ 'mr-2': !hostAppStore.isDistributedBySpeckle }"
|
||||
>
|
||||
{{ hostAppStore.connectorVersion }}
|
||||
</div>
|
||||
<div
|
||||
v-if="!hostAppStore.isDistributedBySpeckle && hostAppStore.hostAppName"
|
||||
v-tippy="
|
||||
`${hostAppStore.hostAppName
|
||||
.charAt(0)
|
||||
.toUpperCase()}${hostAppStore.hostAppName.slice(
|
||||
1
|
||||
)} connector is not distributed by Speckle.`
|
||||
"
|
||||
class="text-xs text-foreground-disabled max-[150px]:hidden mr-1"
|
||||
>
|
||||
<CommonBadge color="secondary">Partner</CommonBadge>
|
||||
</div>
|
||||
<HeaderButton
|
||||
v-if="hostAppStore.isDistributedBySpeckle"
|
||||
v-tippy="'Documentation and help'"
|
||||
@@ -65,7 +82,11 @@
|
||||
class="w-4 text-foreground-disabled group-hover:text-foreground-2"
|
||||
/>
|
||||
</HeaderButton>
|
||||
<HeaderButton v-tippy="'Send us feedback'" @click="openFeedbackDialog()">
|
||||
<HeaderButton
|
||||
v-if="hostAppStore.isDistributedBySpeckle"
|
||||
v-tippy="'Send us feedback'"
|
||||
@click="openFeedbackDialog()"
|
||||
>
|
||||
<ChatBubbleLeftIcon
|
||||
class="w-4 text-foreground-disabled group-hover:text-foreground-2"
|
||||
/>
|
||||
@@ -106,8 +127,9 @@ const { $intercom } = useNuxtApp()
|
||||
|
||||
const openFeedbackDialog = () => {
|
||||
if (
|
||||
hostAppStore.hostAppName?.toLowerCase() === 'revit' &&
|
||||
hostAppStore.hostAppVersion?.includes('2022')
|
||||
(hostAppStore.hostAppName?.toLowerCase() === 'revit' &&
|
||||
hostAppStore.hostAppVersion?.includes('2022')) ||
|
||||
!hostAppStore.isDistributedBySpeckle
|
||||
) {
|
||||
showFeedbackDialog.value = true
|
||||
} else {
|
||||
|
||||
@@ -36,6 +36,23 @@
|
||||
{{ isDarkTheme ? 'Light theme' : 'Dark theme' }}
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
v-if="isDisableCacheSupported"
|
||||
v-slot="{ active }"
|
||||
@click="toggleCache"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
active ? 'bg-highlight-1' : '',
|
||||
'my-1 text-body-2xs flex justify-between px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
|
||||
]"
|
||||
>
|
||||
<span>Disable Cache</span>
|
||||
<span v-if="isCacheDisabled" class="text-primary font-bold ml-2">✓</span>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
<div class="border-t border-outline-3 mt-1">
|
||||
<MenuItem v-if="app.$revitMapperBinding" v-slot="{ active }">
|
||||
<button
|
||||
@@ -120,12 +137,41 @@ import { storeToRefs } from 'pinia'
|
||||
import { XMarkIcon, Bars3Icon } from '@heroicons/vue/20/solid'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||
import { useConfigStore } from '~/store/config'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
|
||||
const app = useNuxtApp()
|
||||
|
||||
const uiConfigStore = useConfigStore()
|
||||
const { isDarkTheme, hasConfigBindings, isDevMode } = storeToRefs(uiConfigStore)
|
||||
const { toggleTheme } = uiConfigStore
|
||||
const { isDarkTheme, hasConfigBindings, isDevMode, isCacheDisabled } =
|
||||
storeToRefs(uiConfigStore)
|
||||
const { toggleTheme, toggleCache } = uiConfigStore
|
||||
|
||||
const hostAppStore = useHostAppStore()
|
||||
const { hostAppName, connectorVersion } = storeToRefs(hostAppStore)
|
||||
|
||||
const isDisableCacheSupported = computed(() => {
|
||||
const appName = hostAppName.value
|
||||
const version = connectorVersion.value
|
||||
|
||||
if (!appName || !version) return false
|
||||
|
||||
// excludes non-sharp connectors (assuming they don't have backend cache bypass)
|
||||
const nonSharpApps = ['sketchup', 'archicad', 'vectorworks']
|
||||
if (nonSharpApps.includes(appName.toLowerCase())) return false
|
||||
|
||||
// always show in dev environments
|
||||
if (version.includes('dev') || version.includes('local') || version.includes('1.0.0'))
|
||||
return true
|
||||
|
||||
// for sharp connectors, check if version is >= 3.18.0
|
||||
const targetVersion = '3.19.0'
|
||||
return (
|
||||
version.localeCompare(targetVersion, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
}) >= 0
|
||||
)
|
||||
})
|
||||
|
||||
const { $showDevTools, $openUrl } = useNuxtApp()
|
||||
const showAccountsDialog = ref(false)
|
||||
|
||||
@@ -5,21 +5,35 @@
|
||||
>
|
||||
Welcome to Speckle
|
||||
</h1>
|
||||
<div v-if="isDesktopServiceAvailable">
|
||||
<AccountsSignInFlow />
|
||||
<div v-if="isDesktopServiceAvailable || canAddAccount">
|
||||
<div class="flex flex-col space-y-4 p-2">
|
||||
<FormTextInput
|
||||
v-model="customServerUrl"
|
||||
name="Server to sign in"
|
||||
:show-label="false"
|
||||
placeholder="https://app.speckle.systems"
|
||||
color="foundation"
|
||||
autocomplete="off"
|
||||
show-clear
|
||||
/>
|
||||
<div class="space-y-2">
|
||||
<AccountsSignInFlow :server-url="customServerUrl" />
|
||||
<AccountsExchangeTokenSignInFlow :server-url="customServerUrl" />
|
||||
<AccountsLegacySignInFlow :server-url="customServerUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="text-foreground-2 mt-2 mb-4">
|
||||
Click the button below to sign into Speckle via Manager. This will allow you to
|
||||
publish or load data.
|
||||
To sign in and start using Speckle, you'll need the Desktop Service running.
|
||||
This lightweight background service handles secure authentication.
|
||||
</div>
|
||||
<div class="text-foreground-2 text-sm mt-2 mb-4"></div>
|
||||
<div class="flex flex-wrap justify-center space-y-2 max-width">
|
||||
<FormButton full-width @click="$openUrl(`speckle://accounts`)">
|
||||
Sign In
|
||||
<div class="space-y-3">
|
||||
<FormButton full-width @click="$openUrl('https://releases.speckle.systems')">
|
||||
Download Desktop Service
|
||||
</FormButton>
|
||||
<div>
|
||||
<div class="text-xs">Already done?</div>
|
||||
<div class="text-center">
|
||||
<div class="text-foreground-2 text-xs mb-2">Already installed?</div>
|
||||
<FormButton
|
||||
size="sm"
|
||||
full-width
|
||||
@@ -27,20 +41,29 @@
|
||||
link
|
||||
@click="accountStore.refreshAccounts()"
|
||||
>
|
||||
Click to refresh
|
||||
Refresh to check again
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAccountStore } from '~~/store/accounts'
|
||||
import { useDesktopService } from '~/lib/core/composables/desktopService'
|
||||
import type { BaseBridge } from '~/lib/bridge/base'
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const { pingDesktopService } = useDesktopService()
|
||||
|
||||
const customServerUrl = ref<string>('https://app.speckle.systems')
|
||||
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
|
||||
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
||||
)
|
||||
|
||||
const isDesktopServiceAvailable = ref(false) // this should be false default because there is a delay if /ping is not successful.
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<!-- CommonTiptapViewer.vue -->
|
||||
<template>
|
||||
<!-- read-only output -->
|
||||
<div
|
||||
v-if="html"
|
||||
class="p-1 pl-3 group w-full whitespace-pre-wrap break-words"
|
||||
v-html="html"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
const props = defineProps<{
|
||||
doc: JSONContent | null | undefined
|
||||
}>()
|
||||
|
||||
const escapeHtml = (str: string): string =>
|
||||
str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
||||
function renderNode(node?: JSONContent): string {
|
||||
if (!node) return ''
|
||||
|
||||
const children = (node.content ?? []).map(renderNode).join('')
|
||||
|
||||
switch (node.type) {
|
||||
case 'doc':
|
||||
return children
|
||||
|
||||
case 'paragraph':
|
||||
// empty paragraph → visual empty line
|
||||
return children ? `<p>${children}</p>` : '<p><br /></p>'
|
||||
|
||||
case 'text': {
|
||||
const text = escapeHtml(node.text ?? '')
|
||||
// if you need marks later (bold, italic, etc.), handle here
|
||||
return text
|
||||
}
|
||||
|
||||
case 'hardBreak':
|
||||
return '<br />'
|
||||
|
||||
default:
|
||||
// unknown node → just render its children
|
||||
return children
|
||||
}
|
||||
}
|
||||
|
||||
const html = computed(() => (props.doc ? renderNode(props.doc) : ''))
|
||||
</script>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="p-0">
|
||||
<slot name="activator" :toggle="toggleDialog"></slot>
|
||||
<CommonDialog v-model:open="showIssuesDialog" :title="`Issues`" fullscreen="none">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div v-if="selectedIssue" class="flex flex-col space-y-1.5">
|
||||
<div class="relative flex items-center h-8">
|
||||
<div class="absolute left-0">
|
||||
<FormButton
|
||||
color="outline"
|
||||
hide-text
|
||||
:icon-left="ArrowLeft"
|
||||
@click="selectedIssue = undefined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto text-foreground-2 font-medium font-mono text-body-xs">
|
||||
{{ selectedIssue.identifier }}
|
||||
</div>
|
||||
<div class="absolute right-0">
|
||||
<FormButton
|
||||
v-tippy="'Open issue in browser'"
|
||||
color="outline"
|
||||
hide-text
|
||||
:icon-left="ArrowTopRightOnSquareIcon"
|
||||
@click="openIssueOnWeb(selectedIssue.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<IssuesSelectedItem :issue="selectedIssue" :model-card="modelCard" />
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedIssue" class="flex flex-col space-y-2">
|
||||
<IssuesItem
|
||||
v-for="issue in issues"
|
||||
:key="issue.id"
|
||||
:issue="issue"
|
||||
:model-card="modelCard"
|
||||
@select="selectedIssue = issue"
|
||||
@open-on-web="(issueId) => openIssueOnWeb(issueId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CommonDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IssuesItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import type { IModelCard } from '~/lib/models/card'
|
||||
import { ArrowLeft } from 'lucide-vue-next'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid'
|
||||
|
||||
const props = defineProps<{
|
||||
issues: IssuesItemFragment[]
|
||||
modelCard: IModelCard
|
||||
}>()
|
||||
|
||||
const app = useNuxtApp()
|
||||
const showIssuesDialog = ref(false)
|
||||
const selectedIssue = ref<IssuesItemFragment | undefined>(undefined)
|
||||
|
||||
const toggleDialog = () => {
|
||||
showIssuesDialog.value = !showIssuesDialog.value
|
||||
}
|
||||
|
||||
const openIssueOnWeb = (issueId: string) => {
|
||||
app.$baseBinding.openUrl(
|
||||
`${props.modelCard.serverUrl}/projects/${props.modelCard?.projectId}/models/${props.modelCard.modelId}#threadId=${issueId}`
|
||||
)
|
||||
}
|
||||
|
||||
watch(showIssuesDialog, (open) => {
|
||||
if (!open) selectedIssue.value = undefined
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<button
|
||||
class="gap-1 border rounded-xl border-outline-3 p-1.5 pt-1 pl-3 group hover:shadow-md hover:cursor-pointer space-y-2"
|
||||
@click="emit('select'), highlightModel()"
|
||||
>
|
||||
<!-- Item Header -->
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-foreground-2 font-medium font-mono text-body-xs">
|
||||
{{ issue.identifier }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<FormButton
|
||||
v-if="store.hostAppName !== 'navisworks' && store.hostAppName !== 'etabs'"
|
||||
v-tippy="'Highlight'"
|
||||
color="subtle"
|
||||
:icon-left="CursorArrowRaysIcon"
|
||||
hide-text
|
||||
size="sm"
|
||||
@click.stop="highlightModel"
|
||||
/>
|
||||
<FormButton
|
||||
v-tippy="'Open issue in browser'"
|
||||
color="subtle"
|
||||
:icon-left="ArrowTopRightOnSquareIcon"
|
||||
hide-text
|
||||
size="sm"
|
||||
class="mr-1"
|
||||
@click.stop="emit('open-on-web', issue.id)"
|
||||
/>
|
||||
<UserAvatar :user="issue.assignee?.user" size="xs" class="rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Item Title & status -->
|
||||
<div class="flex items-center gap-1">
|
||||
<IssuesStatusIcon :status="issue.status" />
|
||||
<div class="line-clamp-2 font-medium text-body-2xs text-foreground">
|
||||
{{ issue.title ? issue.title : 'No title' }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Remaining secondary fields -->
|
||||
<div class="flex items-center gap-4 ml-0.5">
|
||||
<IssuesPriorityIcon :priority="issue.priority" />
|
||||
<IssuesLabels :labels="issue.labels" />
|
||||
<div v-if="formattedDate" class="flex items-center gap-1 h-6">
|
||||
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
|
||||
<span class="text-body-3xs text-foreground-2 font-medium">
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-1 h-6">
|
||||
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
|
||||
<span class="text-body-3xs text-foreground-2 font-medium">No due date</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { IssuesItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { CursorArrowRaysIcon } from '@heroicons/vue/24/outline'
|
||||
import { Calendar } from 'lucide-vue-next'
|
||||
import dayjs from 'dayjs'
|
||||
import { useHostAppStore } from '~~/store/hostApp'
|
||||
import { ToastNotificationType } from '@speckle/ui-components'
|
||||
import type { IModelCard } from '~/lib/models/card'
|
||||
import type { SenderModelCard } from '~/lib/models/card/send'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid'
|
||||
|
||||
const store = useHostAppStore()
|
||||
|
||||
const props = defineProps<{
|
||||
modelCard: IModelCard
|
||||
issue: IssuesItemFragment
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select'): void
|
||||
(e: 'open-on-web', issueId: string): void
|
||||
}>()
|
||||
|
||||
const app = useNuxtApp()
|
||||
|
||||
type IssueViewerState = {
|
||||
ui: {
|
||||
filters: {
|
||||
selectedObjectApplicationIds?: Record<string, string>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const highlightModel = async () => {
|
||||
if (!props.issue.viewerState) {
|
||||
store.setNotification({
|
||||
title: 'Objects not found to highlight',
|
||||
type: ToastNotificationType.Info
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (props.modelCard.typeDiscriminator !== 'SenderModelCard') return
|
||||
|
||||
const sender = props.modelCard as SenderModelCard
|
||||
|
||||
type SelectedObjectMap = Record<string, string>
|
||||
|
||||
const selectedObjectApplicationIds = Object.values(
|
||||
((props.issue.viewerState as IssueViewerState).ui.filters
|
||||
.selectedObjectApplicationIds ?? {}) as SelectedObjectMap
|
||||
)
|
||||
|
||||
const appIdsToHighlight = (sender.sendFilter?.selectedObjectIds ?? []).filter((id) =>
|
||||
selectedObjectApplicationIds.includes(id)
|
||||
)
|
||||
|
||||
if (appIdsToHighlight.length > 0) {
|
||||
await app.$baseBinding.highlightObjects(appIdsToHighlight)
|
||||
} else {
|
||||
store.setNotification({
|
||||
title: 'Objects not found to highlight on this model.',
|
||||
type: ToastNotificationType.Info
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const formattedDate = computed((): string | null => {
|
||||
try {
|
||||
const date = props.issue.dueDate ? dayjs(props.issue.dueDate).toDate() : null
|
||||
|
||||
if (!(date instanceof Date)) return null
|
||||
|
||||
const time = date.getTime()
|
||||
if (isNaN(time)) return null
|
||||
|
||||
return new Intl.DateTimeFormat('en-GB', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).format(date)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="flex items-center -space-x-1">
|
||||
<template
|
||||
v-for="labelItem in maxVisible ? labels.slice(0, maxVisible) : labels"
|
||||
:key="labelItem.id"
|
||||
>
|
||||
<div
|
||||
v-if="labelItem.hexColor"
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
:style="{ backgroundColor: labelItem.hexColor }"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Single label -->
|
||||
<span
|
||||
v-if="labels.length === 1"
|
||||
class="text-body-3xs font-medium flex items-center gap-1"
|
||||
:style="{ color: labels[0].hexColor || undefined }"
|
||||
>
|
||||
{{ labels[0].name }}
|
||||
</span>
|
||||
|
||||
<!-- Multiple labels -->
|
||||
<span v-else class="text-body-3xs text-foreground-2 font-medium">
|
||||
{{ labels.length }} label{{ labels.length !== 1 ? 's' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Label } from '~/lib/issues/types'
|
||||
|
||||
defineProps<{
|
||||
labels: Label[]
|
||||
maxVisible?: number
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<Tippy interactive placement="bottom" :offset="[0, 6]">
|
||||
<!-- Trigger -->
|
||||
<template #default>
|
||||
<IssuesLabelGroup :labels="labels" />
|
||||
</template>
|
||||
|
||||
<!-- Tooltip content -->
|
||||
<template v-if="labels.length > 0" #content>
|
||||
<div class="rounded-md shadow-lg p-0.5 text-xs space-y-1">
|
||||
<div
|
||||
v-for="label in labels"
|
||||
:key="label.id"
|
||||
class="flex items-center space-x-2"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full"
|
||||
:style="{ backgroundColor: label.hexColor }"
|
||||
/>
|
||||
<span>{{ label.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Tippy>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Tippy } from 'vue-tippy'
|
||||
import type { Label } from '~/lib/issues/types'
|
||||
|
||||
defineProps<{
|
||||
labels: Label[]
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
v-if="priority !== null && priority !== 'none'"
|
||||
v-tippy="showLabel ? undefined : priorityText"
|
||||
class="flex flex-col gap-0.5 items-start justify-center w-3 h-3"
|
||||
>
|
||||
<!-- Top line -->
|
||||
<div
|
||||
class="h-0.5 rounded-full bg-foreground-2 w-3"
|
||||
:class="priority !== 'high' && 'opacity-25'"
|
||||
/>
|
||||
<!-- Middle line -->
|
||||
<div
|
||||
class="h-0.5 rounded-full bg-foreground-2 w-2"
|
||||
:class="priority === 'low' && 'opacity-25'"
|
||||
/>
|
||||
<!-- Bottom line -->
|
||||
<div class="h-0.5 rounded-full bg-foreground-2 w-1" />
|
||||
</div>
|
||||
<!-- No priority: Two dashes -->
|
||||
<div v-else class="flex gap-0.5 items-center justify-center h-3 w-3">
|
||||
<div class="h-px rounded-full bg-foreground-3 w-1" />
|
||||
<div class="h-px rounded-full bg-foreground-3 w-1" />
|
||||
</div>
|
||||
|
||||
<span v-if="showLabel" class="text-body-3xs text-foreground-2 font-medium">
|
||||
{{ priorityText }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
priority: 'none' | 'low' | 'medium' | 'high' | null
|
||||
showLabel?: boolean
|
||||
}>(),
|
||||
{
|
||||
showLabel: false
|
||||
}
|
||||
)
|
||||
|
||||
const priorityText = computed(() => {
|
||||
switch (props.priority) {
|
||||
case 'high':
|
||||
return 'High'
|
||||
case 'medium':
|
||||
return 'Medium'
|
||||
case 'low':
|
||||
return 'Low'
|
||||
case 'none':
|
||||
return 'No priority'
|
||||
case null:
|
||||
return 'No priority'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-1.5">
|
||||
<div class="flex flex-col items-start space-y-2 p-2">
|
||||
<div class="line-clamp-2 font-medium text-body text-foreground">
|
||||
{{ issue.title ? issue.title : 'No title' }}
|
||||
</div>
|
||||
<IssuesBasicTiptap
|
||||
v-if="issue.description?.doc"
|
||||
class="border rounded-xl border-outline-3 w-full"
|
||||
:doc="issue.description?.doc"
|
||||
></IssuesBasicTiptap>
|
||||
|
||||
<div v-if="app.$parametersBinding && hasObjectDeltas" class="w-full pt-1 pb-1">
|
||||
<FormButton
|
||||
class="w-full justify-center"
|
||||
:disabled="isApplying || isResolved"
|
||||
@click="applyChanges"
|
||||
>
|
||||
{{
|
||||
isApplying ? 'Applying...' : isResolved ? 'Issue resolved' : 'Apply changes'
|
||||
}}
|
||||
</FormButton>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<IssuesStatusIcon :status="issue.status" show-label />
|
||||
<IssuesPriorityIcon :priority="issue.priority" show-label />
|
||||
<div class="flex items-center justify-between space-x-1">
|
||||
<UserAvatar :user="issue.assignee?.user" size="xs" />
|
||||
<span class="text-body-3xs text-foreground-2 font-medium">
|
||||
{{ issue.assignee ? issue.assignee?.user.name : 'No assignee' }}
|
||||
</span>
|
||||
</div>
|
||||
<IssuesLabels :labels="issue.labels" />
|
||||
<div v-if="formattedDate" class="flex items-center gap-1 h-6">
|
||||
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
|
||||
<span class="text-body-3xs text-foreground-2 font-medium">
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-1 h-6">
|
||||
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
|
||||
<span class="text-body-3xs text-foreground-2 font-medium">No due date</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="issue.activities && issue.activities.totalCount > 0"
|
||||
class="flex items-center gap-2 p-1 min-w-0"
|
||||
>
|
||||
<UserAvatar
|
||||
:user="issue.activities?.items?.[0]?.actor?.user"
|
||||
size="xs"
|
||||
class="shrink-0"
|
||||
/>
|
||||
|
||||
<div class="text-body-2xs text-foreground-2 leading-tight min-w-0">
|
||||
<span class="font-medium">
|
||||
{{ issue.activities?.items?.[0]?.actor?.user.name }}
|
||||
</span>
|
||||
<span>
|
||||
created this issue ·
|
||||
{{ dayjs(issue.activities?.items?.[0].createdAt).from(dayjs()) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="issue.replies && issue.replies.totalCount > 0"
|
||||
class="flex flex-col justify-between space-y-2 w-full"
|
||||
>
|
||||
<div
|
||||
v-for="reply in issue.replies.items"
|
||||
:key="reply.id"
|
||||
class="flex flex-col items-start border rounded-xl border-outline-3 p-1 w-full"
|
||||
>
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<UserAvatar :user="reply.author?.user" size="xs" class="shrink-0" />
|
||||
<div class="text-body-2xs text-foreground-2 leading-tight min-w-0">
|
||||
<span class="font-medium">
|
||||
{{ reply.author?.user.name }}
|
||||
</span>
|
||||
<span>
|
||||
replied ·
|
||||
{{ dayjs(reply.createdAt).from(dayjs()) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IssuesBasicTiptap
|
||||
v-if="reply.description?.doc"
|
||||
class="ml-4"
|
||||
:doc="reply.description?.doc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { ResourceMetaType, IssueStatus } from '~/lib/common/generated/gql/graphql'
|
||||
import { issueResourceMetaSearchQuery } from '~/lib/issues/graphql/queries'
|
||||
import type { IssuesItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import type { IModelCard } from '~/lib/models/card'
|
||||
import dayjs from 'dayjs'
|
||||
import { Calendar } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
issue: IssuesItemFragment
|
||||
modelCard: IModelCard
|
||||
}>()
|
||||
|
||||
const app = useNuxtApp()
|
||||
const isApplying = ref(false)
|
||||
|
||||
const isResolved = computed(() => {
|
||||
return props.issue.status === IssueStatus.Resolved
|
||||
})
|
||||
|
||||
const queryVariables = computed(() => ({
|
||||
workspaceId: props.modelCard.workspaceId!,
|
||||
projectId: props.modelCard.projectId,
|
||||
resourceType: ResourceMetaType.Issue,
|
||||
resourceId: props.issue.id,
|
||||
metaType: 'objectDeltas'
|
||||
}))
|
||||
|
||||
const queryOptions = computed(() => ({
|
||||
fetchPolicy: 'cache-and-network' as const,
|
||||
enabled: !!props.modelCard.workspaceId,
|
||||
clientId: props.modelCard.accountId
|
||||
}))
|
||||
|
||||
const { result: resourceMetaResult } = useQuery(
|
||||
issueResourceMetaSearchQuery,
|
||||
queryVariables,
|
||||
queryOptions
|
||||
)
|
||||
|
||||
const hasObjectDeltas = computed<boolean>(() => {
|
||||
const metadata = resourceMetaResult.value?.resourceMetaSearch
|
||||
return Array.isArray(metadata) && metadata.length > 0
|
||||
})
|
||||
|
||||
const objectDeltasPayload = computed<unknown>(() => {
|
||||
if (!hasObjectDeltas.value) return null
|
||||
const metadata = resourceMetaResult.value?.resourceMetaSearch
|
||||
|
||||
if (Array.isArray(metadata) && metadata.length > 0) {
|
||||
return metadata[0]?.data as unknown
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const applyChanges = async () => {
|
||||
if (!objectDeltasPayload.value) return
|
||||
|
||||
isApplying.value = true
|
||||
try {
|
||||
const payload =
|
||||
typeof objectDeltasPayload.value === 'string'
|
||||
? objectDeltasPayload.value
|
||||
: JSON.stringify(objectDeltasPayload.value)
|
||||
|
||||
if (app.$parametersBinding) {
|
||||
await app.$parametersBinding.update(payload)
|
||||
} else {
|
||||
console.warn('IParametersBinding not available in this host app')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes:', error)
|
||||
} finally {
|
||||
isApplying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formattedDate = computed((): string | null => {
|
||||
try {
|
||||
const date = props.issue.dueDate ? dayjs(props.issue.dueDate).toDate() : null
|
||||
|
||||
if (!(date instanceof Date)) return null
|
||||
|
||||
const time = date.getTime()
|
||||
if (isNaN(time)) return null
|
||||
|
||||
return new Intl.DateTimeFormat('en-GB', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).format(date)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div
|
||||
v-tippy="showLabel ? undefined : statusText"
|
||||
class="flex items-center gap-1 rounded-md hover:bg-foreground-1"
|
||||
>
|
||||
<GlobalIconStatusOpen v-if="status === 'open'" class="w-3 h-3 shrink-0" />
|
||||
<GlobalIconStatusReview
|
||||
v-else-if="status === 'readyForReview'"
|
||||
class="w-3 h-3 shrink-0"
|
||||
/>
|
||||
<GlobalIconStatusResolved
|
||||
v-else-if="status === 'resolved'"
|
||||
class="w-3 h-3 shrink-0"
|
||||
/>
|
||||
|
||||
<span v-if="showLabel" class="text-body-3xs text-foreground-2 font-medium">
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GlobalIconStatusOpen from '~/components/global/icon/StatusOpen.vue'
|
||||
import GlobalIconStatusReview from '~/components/global/icon/StatusReview.vue'
|
||||
import GlobalIconStatusResolved from '~/components/global/icon/StatusResolved.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
status: 'open' | 'readyForReview' | 'resolved'
|
||||
showLabel?: boolean
|
||||
}>(),
|
||||
{
|
||||
showLabel: false
|
||||
}
|
||||
)
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'open':
|
||||
return 'Open'
|
||||
case 'readyForReview':
|
||||
return 'Ready for review'
|
||||
case 'resolved':
|
||||
return 'Resolved'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -5,6 +5,7 @@
|
||||
:icon-left="Bars3Icon"
|
||||
hide-text
|
||||
size="sm"
|
||||
:disabled="!!props.modelCard.progress"
|
||||
@click.stop="openModelCardActionsDialog = true"
|
||||
/>
|
||||
<CommonDialog
|
||||
@@ -32,6 +33,18 @@
|
||||
</button>
|
||||
</template>
|
||||
</ReportBase>
|
||||
<IssuesDialog
|
||||
v-if="issues && issues.length > 0"
|
||||
:model-card="modelCard"
|
||||
:issues="issues"
|
||||
>
|
||||
<template #activator="{ toggle }">
|
||||
<button class="action action-normal" @click="toggle()">
|
||||
<div class="truncate max-[275px]:text-xs">Issues</div>
|
||||
<div><Cog6ToothIcon class="w-5 h-5" /></div>
|
||||
</button>
|
||||
</template>
|
||||
</IssuesDialog>
|
||||
<button
|
||||
v-for="item in items"
|
||||
:key="item.name"
|
||||
@@ -57,6 +70,10 @@ import {
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import type { IModelCard } from '~/lib/models/card'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { issuesListQuery } from '~/lib/issues/graphql/queries'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
|
||||
const { trackEvent } = useMixpanel()
|
||||
|
||||
@@ -113,6 +130,22 @@ const items = [
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const { activeAccount } = storeToRefs(accountStore)
|
||||
const accountId = computed(() => activeAccount.value.accountInfo.id)
|
||||
|
||||
const { result: issuesResult } = useQuery(
|
||||
issuesListQuery,
|
||||
() => ({ projectId: props.modelCard.projectId }),
|
||||
() => ({
|
||||
clientId: accountId.value,
|
||||
debounce: 500,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
)
|
||||
|
||||
const issues = computed(() => issuesResult?.value?.project.issues.items)
|
||||
</script>
|
||||
<style scoped lang="postcss">
|
||||
.action {
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
>
|
||||
<div v-if="modelData" class="relative px-1 py-1">
|
||||
<div class="relative flex items-center space-x-2 min-w-0">
|
||||
<div class="text-foreground-2 mt-[2px] flex items-center -space-x-2 relative">
|
||||
<div
|
||||
v-tippy="buttonTooltip"
|
||||
class="text-foreground-2 mt-[2px] flex items-center -space-x-2 relative"
|
||||
>
|
||||
<!-- CTA button -->
|
||||
<FormButton
|
||||
v-tippy="buttonTooltip"
|
||||
color="outline"
|
||||
:icon-left="
|
||||
modelCard.progress
|
||||
@@ -19,7 +21,9 @@
|
||||
"
|
||||
hide-text
|
||||
class=""
|
||||
:disabled="!canEdit || isSettingsMissing"
|
||||
:disabled="
|
||||
(!canEdit || isSettingsMissing || ctaDisabled) && !modelCard.progress
|
||||
"
|
||||
@click.stop="$emit('manual-publish-or-load')"
|
||||
></FormButton>
|
||||
</div>
|
||||
@@ -67,6 +71,22 @@
|
||||
size="sm"
|
||||
@click="deleteSettings"
|
||||
/> -->
|
||||
<IssuesDialog
|
||||
v-if="issues && issues.length > 0"
|
||||
:model-card="modelCard"
|
||||
:issues="issues"
|
||||
>
|
||||
<template #activator="{ toggle }">
|
||||
<FormButton
|
||||
v-tippy="'Issues'"
|
||||
color="subtle"
|
||||
:icon-left="MessageCircleMore"
|
||||
hide-text
|
||||
size="sm"
|
||||
@click="toggle()"
|
||||
/>
|
||||
</template>
|
||||
</IssuesDialog>
|
||||
<FormButton
|
||||
v-if="store.hostAppName !== 'navisworks' && store.hostAppName !== 'etabs'"
|
||||
v-tippy="'Highlight'"
|
||||
@@ -179,7 +199,7 @@
|
||||
>
|
||||
<div
|
||||
v-tippy="
|
||||
`${latestCommentNotification.comment?.author.name} just left a
|
||||
`${latestCommentNotification.comment?.author?.name} just left a
|
||||
comment.`
|
||||
"
|
||||
class="flex items-center space-x-1"
|
||||
@@ -189,8 +209,8 @@
|
||||
:users="[latestCommentNotification.comment?.author as AvatarUserWithId]"
|
||||
/>
|
||||
<span class="line-clamp-1">
|
||||
{{ latestCommentNotification.comment?.author.name }} just left a
|
||||
comment.
|
||||
{{ latestCommentNotification.comment?.author?.name }} just left a
|
||||
comment on the issue.
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -236,17 +256,26 @@ import type { ProjectCommentsUpdatedMessage } from '~/lib/common/generated/gql/g
|
||||
import { useFunctionRunsStatusSummary } from '~/lib/automate/runStatus'
|
||||
import { CursorArrowRaysIcon, XCircleIcon, TrashIcon } from '@heroicons/vue/24/outline'
|
||||
import type { AvatarUserWithId } from '@speckle/ui-components'
|
||||
import { issuesListQuery } from '~/lib/issues/graphql/queries'
|
||||
import { MessageCircleMore } from 'lucide-vue-next'
|
||||
|
||||
const app = useNuxtApp()
|
||||
const store = useHostAppStore()
|
||||
const accStore = useAccountStore()
|
||||
const { trackEvent } = useMixpanel()
|
||||
|
||||
const props = defineProps<{
|
||||
modelCard: IModelCard
|
||||
project: ProjectModelGroup
|
||||
canEdit: boolean
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelCard: IModelCard
|
||||
project: ProjectModelGroup
|
||||
canEdit: boolean
|
||||
ctaDisabled?: boolean
|
||||
ctaDisabledMessage?: string
|
||||
}>(),
|
||||
{
|
||||
ctaDisabled: false
|
||||
}
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
(e: 'manual-publish-or-load'): void
|
||||
@@ -257,11 +286,9 @@ const isSender = computed(() => {
|
||||
})
|
||||
|
||||
const buttonTooltip = computed(() => {
|
||||
return props.modelCard.progress
|
||||
? 'Cancel'
|
||||
: isSender.value
|
||||
? 'Publish model'
|
||||
: 'Load selected version'
|
||||
if (props.modelCard.progress) return 'Cancel'
|
||||
if (props.ctaDisabled) return props.ctaDisabledMessage
|
||||
return isSender.value ? 'Publish model' : 'Load selected version'
|
||||
})
|
||||
|
||||
const projectAccount = computed(() =>
|
||||
@@ -327,6 +354,25 @@ const summary = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const { result: issuesResult, refetch: refetchIssues } = useQuery(
|
||||
issuesListQuery,
|
||||
() => ({ projectId: props.modelCard.projectId }),
|
||||
() => ({
|
||||
clientId,
|
||||
debounce: 500,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
)
|
||||
|
||||
const issues = computed(() =>
|
||||
issuesResult?.value?.project.issues.items.filter(
|
||||
(issue) =>
|
||||
issue.status !== 'resolved' &&
|
||||
issue.resourceIdString &&
|
||||
(issue.resourceIdString as string).includes(props.modelCard.modelId)
|
||||
)
|
||||
)
|
||||
|
||||
provide<IModelCard>('cardBase', props.modelCard)
|
||||
|
||||
const highlightModel = () => {
|
||||
@@ -486,6 +532,7 @@ onCommentResult((res) => {
|
||||
latestCommentNotification.value = res.data
|
||||
?.projectCommentsUpdated as ProjectCommentsUpdatedMessage
|
||||
startCommentClearTimeout()
|
||||
refetchIssues()
|
||||
})
|
||||
|
||||
const viewComment = () => {
|
||||
|
||||
+98
-25
@@ -4,6 +4,8 @@
|
||||
:model-card="modelCard"
|
||||
:project="project"
|
||||
:can-edit="canEdit"
|
||||
:cta-disabled="ctaDisabled"
|
||||
:cta-disabled-message="ctaDisabledMessage"
|
||||
@manual-publish-or-load="sendOrCancel"
|
||||
>
|
||||
<div class="flex max-[275px]:w-full overflow-hidden my-2">
|
||||
@@ -17,7 +19,6 @@
|
||||
full-width
|
||||
@click.stop="openFilterDialog = true"
|
||||
>
|
||||
<!-- Sending -->
|
||||
<span class="font-bold">{{ modelCard.sendFilter?.name }}: </span>
|
||||
<span class="truncate">{{ modelCard.sendFilter?.summary }}</span>
|
||||
</FormButton>
|
||||
@@ -31,13 +32,23 @@
|
||||
<FilterListSelect :filter="modelCard.sendFilter" @update:filter="updateFilter" />
|
||||
|
||||
<div class="mt-4 flex justify-end items-center space-x-2">
|
||||
<!-- TODO: Ux wise, users might want to just save the selection and publish it later. -->
|
||||
<FormButton size="sm" color="outline" @click.stop="saveFilter()">
|
||||
<FormButton
|
||||
size="sm"
|
||||
color="outline"
|
||||
:disabled="isSaveDisabled"
|
||||
@click.stop="saveFilter()"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
<FormButton size="sm" @click.stop="saveFilterAndSend()">
|
||||
Save & Publish
|
||||
</FormButton>
|
||||
<div v-tippy="!canCreateVersionPerm ? canCreateVersionMessage : ''">
|
||||
<FormButton
|
||||
size="sm"
|
||||
:disabled="!canCreateVersionPerm || isSaveDisabled"
|
||||
@click.stop="saveFilterAndSend()"
|
||||
>
|
||||
Save & Publish
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</CommonDialog>
|
||||
|
||||
@@ -108,7 +119,7 @@
|
||||
</ModelCardBase>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import ModelCardBase from '~/components/model/CardBase.vue'
|
||||
import { Square3Stack3DIcon } from '@heroicons/vue/20/solid'
|
||||
import type { ModelCardNotification } from '~/lib/models/card/notification'
|
||||
@@ -117,13 +128,22 @@ import type { ProjectModelGroup } from '~/store/hostApp'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { ToastNotificationType, ValidationHelpers } from '@speckle/ui-components'
|
||||
import { provideApolloClient, useMutation } from '@vue/apollo-composable'
|
||||
import {
|
||||
provideApolloClient,
|
||||
useMutation,
|
||||
useSubscription
|
||||
} from '@vue/apollo-composable'
|
||||
import { useAccountStore, type DUIAccount } from '~/store/accounts'
|
||||
import { setVersionMessageMutation } from '~/lib/graphql/mutationsAndQueries'
|
||||
const hostAppStore = useHostAppStore()
|
||||
import { workspacePlanUsageUpdatedSubscription } from '~/lib/workspaces/graphql/subscriptions'
|
||||
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
|
||||
|
||||
const store = useHostAppStore()
|
||||
const accountStore = useAccountStore()
|
||||
|
||||
const { trackEvent } = useMixpanel()
|
||||
const app = useNuxtApp()
|
||||
const { canCreateModelIngestion } = useCheckGraphql()
|
||||
|
||||
const cardBase = ref<InstanceType<typeof ModelCardBase>>()
|
||||
const props = defineProps<{
|
||||
@@ -132,37 +152,87 @@ const props = defineProps<{
|
||||
canEdit: boolean
|
||||
}>()
|
||||
|
||||
const store = useHostAppStore()
|
||||
const account = accountStore.accounts.find(
|
||||
(acc) => acc.accountInfo.id === props.project.accountId
|
||||
) as DUIAccount
|
||||
const clientId = account.accountInfo.id
|
||||
|
||||
const openFilterDialog = ref(false)
|
||||
app.$baseBinding?.on('documentChanged', () => {
|
||||
openFilterDialog.value = false
|
||||
})
|
||||
|
||||
const canCreateVersionPerm = ref(true)
|
||||
const canCreateVersionMessage = ref<string | null>(null)
|
||||
|
||||
const checkPermissions = async () => {
|
||||
const res = await canCreateModelIngestion(
|
||||
props.modelCard.projectId,
|
||||
props.modelCard.modelId,
|
||||
props.modelCard.accountId
|
||||
)
|
||||
if (res.queryAvailable) {
|
||||
canCreateVersionPerm.value = res.authorized
|
||||
canCreateVersionMessage.value = res.message || null
|
||||
}
|
||||
}
|
||||
|
||||
const ctaDisabled = computed(
|
||||
() => !canCreateVersionPerm.value || !!props.modelCard.progress
|
||||
)
|
||||
const ctaDisabledMessage = computed(() => canCreateVersionMessage.value || undefined)
|
||||
|
||||
const { onResult: onWorkspacePlanUsageUpdated } = useSubscription(
|
||||
workspacePlanUsageUpdatedSubscription,
|
||||
() => ({
|
||||
input: {
|
||||
workspaceId: props.modelCard.workspaceId as string
|
||||
}
|
||||
}),
|
||||
() => ({ clientId })
|
||||
)
|
||||
|
||||
onWorkspacePlanUsageUpdated(() => {
|
||||
void checkPermissions()
|
||||
})
|
||||
|
||||
const sendOrCancel = () => {
|
||||
if (!props.canEdit) {
|
||||
// check for progress first to allow cancelling even if permissions changed
|
||||
if (props.modelCard.progress) {
|
||||
store.sendModelCancel(props.modelCard.modelCardId)
|
||||
return
|
||||
}
|
||||
if (props.modelCard.progress) store.sendModelCancel(props.modelCard.modelCardId)
|
||||
else store.sendModel(props.modelCard.modelCardId, 'ModelCardButton')
|
||||
|
||||
if (!props.canEdit || !canCreateVersionPerm.value) {
|
||||
return
|
||||
}
|
||||
|
||||
store.sendModel(props.modelCard.modelCardId, 'ModelCardButton')
|
||||
hasSetVersionMessage.value = false
|
||||
}
|
||||
|
||||
let newFilter: ISendFilter
|
||||
const newFilter = ref<ISendFilter>()
|
||||
const updateFilter = (filter: ISendFilter) => {
|
||||
newFilter = filter
|
||||
newFilter.value = filter
|
||||
}
|
||||
|
||||
const isSaveDisabled = computed(() => {
|
||||
const filterToCheck = newFilter.value || props.modelCard.sendFilter
|
||||
return !store.validateSendFilter(filterToCheck).valid
|
||||
})
|
||||
|
||||
const saveFilter = async () => {
|
||||
if (!newFilter.value) return // Safety check
|
||||
void trackEvent('DUI3 Action', {
|
||||
name: 'Publish Card Filter Change',
|
||||
filter: newFilter.typeDiscriminator
|
||||
filter: newFilter.value.typeDiscriminator
|
||||
})
|
||||
|
||||
// do not reset idmap while creating a new one because it is managed by host app
|
||||
newFilter.idMap = props.modelCard.sendFilter?.idMap
|
||||
newFilter.value.idMap = props.modelCard.sendFilter?.idMap
|
||||
|
||||
await store.patchModel(props.modelCard.modelCardId, {
|
||||
sendFilter: newFilter,
|
||||
sendFilter: newFilter.value,
|
||||
expired: true
|
||||
})
|
||||
openFilterDialog.value = false
|
||||
@@ -173,11 +243,6 @@ const isUpdatingVersionMessage = ref(false)
|
||||
const hasSetVersionMessage = ref(false)
|
||||
const versionMessage = ref<string>()
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const account = accountStore.accounts.find(
|
||||
(acc) => acc.accountInfo.id === props.project.accountId
|
||||
) as DUIAccount
|
||||
|
||||
const setVersionMessage = async (message: string) => {
|
||||
if (!props.modelCard.latestCreatedVersionId) {
|
||||
return
|
||||
@@ -203,14 +268,14 @@ const setVersionMessage = async (message: string) => {
|
||||
if (res?.data?.versionMutations.update.id) {
|
||||
// seemed to noisy, and autoclose does not work for some reason.
|
||||
// nicer ux to just close the dialog
|
||||
// hostAppStore.setNotification({
|
||||
// store.setNotification({
|
||||
// type: ToastNotificationType.Info,
|
||||
// title: 'Version message saved',
|
||||
// autoClose: true
|
||||
// })
|
||||
hasSetVersionMessage.value = true
|
||||
} else {
|
||||
hostAppStore.setNotification({
|
||||
store.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Request failed',
|
||||
description: 'Failed to update version message.',
|
||||
@@ -261,6 +326,10 @@ const expiredNotification = computed(() => {
|
||||
const ctaType = props.modelCard.progress ? 'Restart' : 'Update'
|
||||
notification.cta = {
|
||||
name: ctaType,
|
||||
disabled: !canCreateVersionPerm.value,
|
||||
tooltipText: !canCreateVersionPerm.value
|
||||
? canCreateVersionMessage.value || 'Publish limit reached'
|
||||
: undefined,
|
||||
action: async () => {
|
||||
hasSetVersionMessage.value = false
|
||||
if (props.modelCard.progress) {
|
||||
@@ -337,4 +406,8 @@ const latestVersionNotification = computed(() => {
|
||||
}
|
||||
return notification
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void checkPermissions()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
:title="title"
|
||||
:show-back-button="step !== 1"
|
||||
@back="step--"
|
||||
@fully-closed="step = 1"
|
||||
@fully-closed="
|
||||
() => {
|
||||
step = 1
|
||||
settingsWereChanged = false
|
||||
}
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<div v-if="step === 1">
|
||||
@@ -56,11 +61,13 @@ import { useHostAppStore } from '~/store/hostApp'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { ReceiverModelCard } from '~/lib/models/card/receiver'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { useSettingsTracking } from '~/lib/core/composables/trackSettings'
|
||||
import { useAddByUrl } from '~/lib/core/composables/addByUrl'
|
||||
import { getSlugFromHostAppNameAndVersion } from '~/lib/common/helpers/hostAppSlug'
|
||||
import type { CardSetting } from '~/lib/models/card/setting'
|
||||
|
||||
const { trackEvent } = useMixpanel()
|
||||
const { trackSettingsChange } = useSettingsTracking()
|
||||
|
||||
const showReceiveDialog = defineModel<boolean>('open', { default: false })
|
||||
|
||||
@@ -86,6 +93,7 @@ const selectedWorkspace = ref<WorkspaceListWorkspaceItemFragment>()
|
||||
const selectedProject = ref<ProjectListProjectItemFragment>()
|
||||
const selectedModel = ref<ModelListModelItemFragment>()
|
||||
const receieveSettings = ref<CardSetting[] | undefined>(undefined)
|
||||
const settingsWereChanged = ref(false)
|
||||
|
||||
const { tryParseUrl, urlParsedData, urlParseError } = useAddByUrl()
|
||||
const updateSearchText = (text: string | undefined) => {
|
||||
@@ -136,6 +144,7 @@ const title = computed(() => {
|
||||
|
||||
const handleUpdateSettings = (settings: CardSetting[]) => {
|
||||
receieveSettings.value = settings
|
||||
settingsWereChanged.value = true
|
||||
}
|
||||
|
||||
// accountId, serverUrl, ModelListModelItemFragment, VersionListItemFragment
|
||||
@@ -155,23 +164,39 @@ const selectVersionAndAddModel = async (
|
||||
m.typeDiscriminator === 'ReceiverModelCard'
|
||||
) as ReceiverModelCard
|
||||
|
||||
// track settings only if user changed them on receive
|
||||
// compare against existing model settings if it exists, otherwise compare against defaults
|
||||
if (settingsWereChanged.value && receieveSettings.value) {
|
||||
trackSettingsChange(
|
||||
'Load Settings Changed',
|
||||
receieveSettings.value,
|
||||
existingModel?.settings || hostAppStore.receiveSettings || [],
|
||||
selectedAccountId.value,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
if (existingModel) {
|
||||
emit('close')
|
||||
// Patch the existing model card with new versions!
|
||||
await hostAppStore.patchModel(existingModel.modelCardId, {
|
||||
const patchPayload: Record<string, unknown> = {
|
||||
selectedVersionId: version.id,
|
||||
selectedVersionSourceApp: version.sourceApplication,
|
||||
selectedVersionUserId: version.authorUser?.id,
|
||||
latestVersionId: latestVersion.id,
|
||||
latestVersionSourceApp: latestVersion.sourceApplication,
|
||||
latestVersionUserId: latestVersion.authorUser?.id
|
||||
})
|
||||
}
|
||||
|
||||
// apply new settings to the existing model card if they were changed
|
||||
if (settingsWereChanged.value && receieveSettings.value) {
|
||||
patchPayload.settings = receieveSettings.value
|
||||
}
|
||||
|
||||
// patch the existing model card with new versions and settings
|
||||
await hostAppStore.patchModel(existingModel.modelCardId, patchPayload)
|
||||
await hostAppStore.receiveModel(existingModel.modelCardId, 'Wizard')
|
||||
return
|
||||
}
|
||||
|
||||
// We were tracking the source host app wrong before `getHostAppFromString`
|
||||
// i.e. we were having `Revit 2023` instead of `revit`
|
||||
const selectedVersionSourceApp = getSlugFromHostAppNameAndVersion(
|
||||
version.sourceApplication as string
|
||||
)
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { useSettingsTracking } from '~/lib/core/composables/trackSettings'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
import type { CardSetting } from '~/lib/models/card/setting'
|
||||
|
||||
const { trackEvent } = useMixpanel()
|
||||
const { trackSettingsChange } = useSettingsTracking()
|
||||
|
||||
const props = defineProps<{
|
||||
settings?: CardSetting[]
|
||||
@@ -48,9 +48,12 @@ const updateSettings = (settings: CardSetting[]) => {
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
void trackEvent('DUI3 Action', {
|
||||
name: 'Send Settings Updated'
|
||||
})
|
||||
trackSettingsChange(
|
||||
'Model Card Settings Updated',
|
||||
newSettings,
|
||||
store.sendSettings || []
|
||||
)
|
||||
|
||||
await store.patchModel(props.modelCardId, {
|
||||
settings: newSettings,
|
||||
expired: true
|
||||
|
||||
+126
-20
@@ -5,7 +5,12 @@
|
||||
:title="title"
|
||||
:show-back-button="step !== 1"
|
||||
@back="step--"
|
||||
@fully-closed="step = 1"
|
||||
@fully-closed="
|
||||
() => {
|
||||
step = 1
|
||||
settingsWereChanged = false
|
||||
}
|
||||
"
|
||||
>
|
||||
<div v-if="step === 1">
|
||||
<WizardProjectSelector
|
||||
@@ -16,7 +21,6 @@
|
||||
@search-text-update="updateSearchText"
|
||||
/>
|
||||
</div>
|
||||
<!-- Model selector wizard -->
|
||||
<div v-if="step === 2 && selectedProject && selectedAccountId">
|
||||
<WizardModelSelector
|
||||
:project="selectedProject"
|
||||
@@ -27,15 +31,26 @@
|
||||
@next="selectModel"
|
||||
/>
|
||||
</div>
|
||||
<!-- Version selector wizard -->
|
||||
<div v-if="step === 3">
|
||||
<SendFiltersAndSettings
|
||||
v-model="filter"
|
||||
@update:filter="(f) => (filter = f)"
|
||||
@update:settings="(s) => (settings = s)"
|
||||
@update:settings="
|
||||
(s) => {
|
||||
settings = s
|
||||
settingsWereChanged = true
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="mt-2">
|
||||
<FormButton full-width @click="addModel">Publish</FormButton>
|
||||
<div v-tippy="publishTooltipMessage" class="mt-2">
|
||||
<FormButton
|
||||
full-width
|
||||
:disabled="isPublishDisabled"
|
||||
:loading="isLoadingPermissions"
|
||||
@click="addModel"
|
||||
>
|
||||
Publish
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="urlParseError" class="p-2 text-danger">
|
||||
@@ -45,6 +60,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSubscription } from '@vue/apollo-composable'
|
||||
import type {
|
||||
ModelListModelItemFragment,
|
||||
ProjectListProjectItemFragment
|
||||
@@ -53,11 +69,16 @@ import type { ISendFilter } from '~/lib/models/card/send'
|
||||
import { SenderModelCard } from '~/lib/models/card/send'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { useSelectionStore } from '~/store/selection'
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import { useSettingsTracking } from '~/lib/core/composables/trackSettings'
|
||||
import type { CardSetting } from '~/lib/models/card/setting'
|
||||
import { useAddByUrl } from '~/lib/core/composables/addByUrl'
|
||||
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
|
||||
import { workspacePlanUsageUpdatedSubscription } from '~/lib/workspaces/graphql/subscriptions'
|
||||
|
||||
const { trackEvent } = useMixpanel()
|
||||
const { trackSettingsChange } = useSettingsTracking()
|
||||
|
||||
const showSendDialog = defineModel<boolean>('open', { default: false })
|
||||
|
||||
@@ -72,8 +93,32 @@ const selectedProject = ref<ProjectListProjectItemFragment>()
|
||||
const selectedModel = ref<ModelListModelItemFragment>()
|
||||
const filter = ref<ISendFilter | undefined>(undefined)
|
||||
const settings = ref<CardSetting[] | undefined>(undefined)
|
||||
const settingsWereChanged = ref(false)
|
||||
|
||||
const { tryParseUrl, urlParsedData, urlParseError } = useAddByUrl()
|
||||
const { canCreateModelIngestion, canCreateVersion } = useCheckGraphql()
|
||||
|
||||
const canPublish = ref(false)
|
||||
const publishLimitMessage = ref<string | undefined>(undefined)
|
||||
const isLoadingPermissions = ref(false)
|
||||
const hostAppStore = useHostAppStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
|
||||
const publishValidation = computed(() => hostAppStore.validateSendFilter(filter.value))
|
||||
|
||||
const isPublishDisabled = computed(() => {
|
||||
return (
|
||||
!canPublish.value || isLoadingPermissions.value || !publishValidation.value.valid
|
||||
)
|
||||
})
|
||||
|
||||
const publishTooltipMessage = computed(() => {
|
||||
if (!publishValidation.value.valid) return publishValidation.value.reason
|
||||
if (!canPublish.value && !isLoadingPermissions.value)
|
||||
return publishLimitMessage.value || ''
|
||||
return ''
|
||||
})
|
||||
|
||||
const updateSearchText = (text: string | undefined) => {
|
||||
urlParseError.value = undefined
|
||||
if (!text) return
|
||||
@@ -89,9 +134,72 @@ watch(urlParsedData, (newVal) => {
|
||||
watch(showSendDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
urlParseError.value = undefined
|
||||
void selectionStore.refreshSelectionFromHostApp()
|
||||
}
|
||||
})
|
||||
|
||||
const checkPermissions = async () => {
|
||||
if (!selectedProject.value || !selectedModel.value) return
|
||||
|
||||
isLoadingPermissions.value = true
|
||||
|
||||
try {
|
||||
const res = await canCreateModelIngestion(
|
||||
selectedProject.value.id,
|
||||
selectedModel.value.id,
|
||||
selectedAccountId.value
|
||||
)
|
||||
if (res.queryAvailable) {
|
||||
canPublish.value = res.authorized
|
||||
publishLimitMessage.value = res.message || undefined
|
||||
} else {
|
||||
// check legacy canCreateVersion in else block
|
||||
const legacyRes = await canCreateVersion(
|
||||
selectedProject.value.id,
|
||||
selectedModel.value.id,
|
||||
selectedAccountId.value
|
||||
)
|
||||
canPublish.value = legacyRes.authorized
|
||||
publishLimitMessage.value = legacyRes.message || undefined
|
||||
}
|
||||
} finally {
|
||||
isLoadingPermissions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(step, async (newVal, oldVal) => {
|
||||
if (newVal > oldVal) {
|
||||
if (newVal === 3) {
|
||||
await checkPermissions()
|
||||
}
|
||||
return // exit fast on forward
|
||||
}
|
||||
if (newVal === 1) {
|
||||
selectedProject.value = undefined
|
||||
selectedModel.value = undefined
|
||||
}
|
||||
if (newVal === 2) selectedModel.value = undefined
|
||||
})
|
||||
|
||||
const workspaceId = computed(() => selectedProject.value?.workspace?.id)
|
||||
|
||||
const { onResult: onUsageUpdate } = useSubscription(
|
||||
workspacePlanUsageUpdatedSubscription,
|
||||
() => ({
|
||||
input: {
|
||||
workspaceId: workspaceId.value || ''
|
||||
}
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!workspaceId.value && step.value === 3,
|
||||
clientId: selectedAccountId.value
|
||||
})
|
||||
)
|
||||
|
||||
onUsageUpdate(() => {
|
||||
void checkPermissions()
|
||||
})
|
||||
|
||||
const selectProject = (accountId: string, project: ProjectListProjectItemFragment) => {
|
||||
step.value++
|
||||
selectedAccountId.value = accountId
|
||||
@@ -112,20 +220,6 @@ const selectModel = (model: ModelListModelItemFragment) => {
|
||||
void trackEvent('DUI3 Action', { name: 'Publish Wizard', step: 'model selected' })
|
||||
}
|
||||
|
||||
// Clears data if going backwards in the wizard
|
||||
watch(step, (newVal, oldVal) => {
|
||||
if (newVal > oldVal) {
|
||||
return // exit fast on forward
|
||||
}
|
||||
if (newVal === 1) {
|
||||
selectedProject.value = undefined
|
||||
selectedModel.value = undefined
|
||||
}
|
||||
if (newVal === 2) selectedModel.value = undefined
|
||||
})
|
||||
|
||||
const hostAppStore = useHostAppStore()
|
||||
|
||||
// accountId, serverUrl, projectId, modelId, sendFilter, settings
|
||||
const addModel = async () => {
|
||||
void trackEvent('DUI3 Action', {
|
||||
@@ -139,6 +233,18 @@ const addModel = async () => {
|
||||
m.modelId === selectedModel.value?.id &&
|
||||
m.typeDiscriminator.includes('SenderModelCard')
|
||||
) as SenderModelCard
|
||||
|
||||
// track settings only if user changed them
|
||||
// compare against existing model card settings
|
||||
if (settingsWereChanged.value && settings.value) {
|
||||
trackSettingsChange(
|
||||
'Publish Settings Changed',
|
||||
settings.value,
|
||||
existingModel?.settings || hostAppStore.sendSettings || [],
|
||||
selectedAccountId.value,
|
||||
true
|
||||
)
|
||||
}
|
||||
if (existingModel) {
|
||||
emit('close')
|
||||
// Patch the existing model card with new send filter and non-expired state!
|
||||
|
||||
@@ -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 | null | undefined
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -32,33 +32,6 @@
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
canCreateModelResult &&
|
||||
!canCreateModelResult.project.permissions.canCreateModel.authorized
|
||||
"
|
||||
>
|
||||
<CommonAlert title="Cannot create new models" color="info" hide-icon>
|
||||
<template #description>
|
||||
{{ canCreateModelResult.project.permissions.canCreateModel.message }}
|
||||
|
||||
<FormButton
|
||||
v-if="workspaceSlug"
|
||||
full-width
|
||||
color="primary"
|
||||
size="sm"
|
||||
class="mt-2"
|
||||
@click="
|
||||
$openUrl(
|
||||
`${account.accountInfo.serverInfo.url}/settings/workspaces/${workspaceSlug}/billing`
|
||||
)
|
||||
"
|
||||
>
|
||||
Explore Plans
|
||||
</FormButton>
|
||||
</template>
|
||||
</CommonAlert>
|
||||
</div>
|
||||
<div class="relative grid grid-cols-1 gap-2">
|
||||
<CommonLoadingBar v-if="loading" loading />
|
||||
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
<div class="space-y-2 relative">
|
||||
<div v-if="workspacesEnabled && workspaces" class="flex items-center space-x-2">
|
||||
<div class="flex-grow min-w-0">
|
||||
<!-- NO WORKSPACE YET -->
|
||||
<div v-if="workspaces.length === 0">
|
||||
<FormButton
|
||||
full-width
|
||||
class="flex items-center"
|
||||
@click="$openUrl('https://app.speckle.systems/workspaces/actions/create')"
|
||||
@click="
|
||||
$openUrl(
|
||||
`${activeAccount.accountInfo.serverInfo.url.replace(
|
||||
/\/$/,
|
||||
''
|
||||
)}/workspaces/actions/create`
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="min-w-0 truncate flex-grow">
|
||||
<span>{{ 'Create a workspace' }}</span>
|
||||
@@ -31,7 +37,7 @@
|
||||
<WorkspaceAvatar
|
||||
:size="'xs'"
|
||||
:name="selectedWorkspace.name || ''"
|
||||
:logo="selectedWorkspace.logo"
|
||||
:logo="selectedWorkspace.logoUrl"
|
||||
/>
|
||||
<div class="min-w-0 truncate flex-grow text-left">
|
||||
<span>{{ selectedWorkspace.name }}</span>
|
||||
@@ -72,13 +78,7 @@
|
||||
color="foundation"
|
||||
/>
|
||||
<div class="flex justify-between items-center space-x-2">
|
||||
<div
|
||||
v-tippy="
|
||||
canCreateProject
|
||||
? 'Create new project'
|
||||
: canCreateProjectPermissionCheck?.message
|
||||
"
|
||||
>
|
||||
<div v-if="canCreateProject" v-tippy="'Create new project'">
|
||||
<FormButton
|
||||
color="outline"
|
||||
:disabled="!canCreateProject"
|
||||
@@ -88,6 +88,22 @@
|
||||
<PlusIcon class="w-4 -mx-2" />
|
||||
</FormButton>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-tippy="
|
||||
canCreateProject
|
||||
? 'Create new project'
|
||||
: canCreateProjectPermissionCheck?.message
|
||||
"
|
||||
>
|
||||
<FormButton
|
||||
color="primary"
|
||||
:class="`p-1.5 bg-foundation rounded text-foreground border`"
|
||||
@click="upgradePlanButtonAction"
|
||||
>
|
||||
<ArrowUpCircleIcon class="w-4 -mx-2" />
|
||||
</FormButton>
|
||||
</div>
|
||||
<CommonDialog
|
||||
v-model:open="showProjectCreateDialog"
|
||||
:title="`Create new project`"
|
||||
@@ -132,28 +148,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
canCreateProjectPermissionCheck &&
|
||||
!canCreateProjectPermissionCheck.authorized
|
||||
"
|
||||
>
|
||||
<CommonAlert color="info" hide-icon>
|
||||
<template #description>
|
||||
{{ canCreateProjectPermissionCheck.message }}
|
||||
<FormButton
|
||||
v-if="showUpgradeButton"
|
||||
full-width
|
||||
class="mt-2"
|
||||
color="primary"
|
||||
size="sm"
|
||||
@click="upgradeButtonAction()"
|
||||
>
|
||||
Upgrade now
|
||||
</FormButton>
|
||||
</template>
|
||||
</CommonAlert>
|
||||
</div>
|
||||
|
||||
<WizardPersonalProjectsWarning v-if="isPersonalProjectsAsWorkspace" />
|
||||
|
||||
@@ -202,7 +196,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { PlusIcon } from '@heroicons/vue/20/solid'
|
||||
import { PlusIcon, ArrowUpCircleIcon } from '@heroicons/vue/20/solid'
|
||||
import type { DUIAccount } from '~/store/accounts'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import {
|
||||
@@ -330,19 +324,18 @@ const activeWorkspace = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
const activeWorkspace = activeWorkspaceResult.value?.activeUser
|
||||
const activeLimitedWorkspace = activeWorkspaceResult.value?.activeUser
|
||||
?.activeWorkspace as WorkspaceListWorkspaceItemFragment
|
||||
|
||||
// fallback to activeWorkspace query result
|
||||
if (activeWorkspace) {
|
||||
return activeWorkspace
|
||||
if (activeLimitedWorkspace) {
|
||||
const activeWorkspace = workspaces.value?.find(
|
||||
(w) => w.id === activeLimitedWorkspace.id
|
||||
)
|
||||
if (activeWorkspace) return activeWorkspace
|
||||
}
|
||||
|
||||
// if activeWorkspace is null will mean that it is personal projects - this fallback wont be the case soon
|
||||
return {
|
||||
id: 'personalProject',
|
||||
name: 'Personal Projects'
|
||||
} as WorkspaceListWorkspaceItemFragment
|
||||
return workspaces.value?.[0] // fallback to first workspace if none is active
|
||||
})
|
||||
|
||||
const selectedWorkspace = ref<WorkspaceListWorkspaceItemFragment | undefined>(
|
||||
@@ -494,35 +487,6 @@ const canCreateProjectPermissionCheck = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
const upgradeButtonAction = () => {
|
||||
if (!canCreateProjectPermissionCheck.value) return
|
||||
if (canCreateProjectPermissionCheck.value.code === 'WorkspaceNoEditorSeat') {
|
||||
// open url to workspace/settings/users
|
||||
$openUrl(
|
||||
`${account.value.accountInfo.serverInfo.url}/settings/workspaces/${selectedWorkspace.value?.slug}/members`
|
||||
)
|
||||
return
|
||||
}
|
||||
if (canCreateProjectPermissionCheck.value.code === 'WorkspaceLimitsReached') {
|
||||
// open url to workspace/billing
|
||||
$openUrl(
|
||||
`${account.value.accountInfo.serverInfo.url}/settings/workspaces/${selectedWorkspace.value?.slug}/billing`
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const showUpgradeButton = computed(() => {
|
||||
if (!canCreateProjectPermissionCheck.value) return false
|
||||
if (
|
||||
canCreateProjectPermissionCheck.value.code === 'WorkspaceNoEditorSeat' ||
|
||||
canCreateProjectPermissionCheck.value.code === 'WorkspaceLimitsReached'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const isCreatingProject = ref(false)
|
||||
const showProjectCreateDialog = ref(false)
|
||||
|
||||
@@ -611,6 +575,31 @@ const createNewPersonalProject = async (name: string) => {
|
||||
isCreatingProject.value = false
|
||||
}
|
||||
|
||||
const upgradePlanButtonAction = () => {
|
||||
if (!canCreateProjectPermissionCheck.value) return
|
||||
if (canCreateProjectPermissionCheck.value.code === 'WorkspaceNoEditorSeat') {
|
||||
// open url to workspace/settings/users
|
||||
$openUrl(
|
||||
`${account.value.accountInfo.serverInfo.url}/settings/workspaces/${selectedWorkspace.value?.slug}/members`
|
||||
)
|
||||
return
|
||||
}
|
||||
if (canCreateProjectPermissionCheck.value.code === 'WorkspaceLimitsReached') {
|
||||
// open url to workspace/billing
|
||||
$openUrl(
|
||||
`${account.value.accountInfo.serverInfo.url}/settings/workspaces/${selectedWorkspace.value?.slug}/billing`
|
||||
)
|
||||
return
|
||||
}
|
||||
// catch SSO session expired / any other unhandled permission flags
|
||||
// redirecting to the workspace root will trigger the standard web authentication flow.
|
||||
if (selectedWorkspace.value?.slug) {
|
||||
$openUrl(
|
||||
`${account.value.accountInfo.serverInfo.url}/workspaces/${selectedWorkspace.value?.slug}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
fetchMore({
|
||||
variables: { cursor: projectsResult.value?.activeUser?.projects.cursor },
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<WorkspaceAvatar
|
||||
:size="'sm'"
|
||||
:name="workspace.name || ''"
|
||||
:logo="workspace.logo"
|
||||
:logo="workspace.logoUrl"
|
||||
/>
|
||||
<div class="min-w-0 grow">
|
||||
<div class="truncate overflow-hidden min-w-0 flex items-center space-x-2">
|
||||
|
||||
@@ -33,13 +33,7 @@ defineEmits<{
|
||||
(e: 'workspace:selected', result: WorkspaceListWorkspaceItemFragment): void
|
||||
}>()
|
||||
|
||||
const workspacesWithPersonalProjects = computed(() => [
|
||||
...props.workspaces.filter((w) => w.creationState?.completed !== false),
|
||||
{
|
||||
id: 'personalProject',
|
||||
name: 'Personal Projects'
|
||||
} as WorkspaceListWorkspaceItemFragment
|
||||
])
|
||||
const workspacesWithPersonalProjects = computed(() => [...props.workspaces])
|
||||
|
||||
const toggleDialog = () => {
|
||||
showWorkspaceSelectorDialog.value = !showWorkspaceSelectorDialog.value
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM node:22-bookworm@sha256:7e791fc54bd02fc89fd4fb39eb37e5bea753c75679c8022478d81679367d995a AS build-stage
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
COPY .yarnrc.yml .
|
||||
RUN yarn install --immutable || (cat /tmp/xfs-*/build.log && exit 1)
|
||||
COPY . .
|
||||
# NODE_ENV must be set after the dependencies are installed because @nuxt/kit is a devDependency and is required to build the application
|
||||
ARG NODE_ENV=production
|
||||
ENV NODE_ENV=${NODE_ENV}
|
||||
ENV NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
|
||||
ENV NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
|
||||
RUN yarn generate
|
||||
|
||||
FROM joseluisq/static-web-server:2.40@sha256:63528bfba5d86b00572e23b4e44ed0f7a791f931df650125156d0c24f7a8f877 AS production-stage
|
||||
WORKDIR /app
|
||||
COPY --from=build-stage /app/dist /app/dist
|
||||
CMD ["--config-file", "/app/configuration.toml"]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: speckle-dui-chart
|
||||
description: A Helm chart for deploying the Speckle DUI3 application
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "0.1.0"
|
||||
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "speckle-dui.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "speckle-dui.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "speckle-dui.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "speckle-dui.labels" -}}
|
||||
helm.sh/chart: {{ include "speckle-dui.chart" . }}
|
||||
{{ include "speckle-dui.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "speckle-dui.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "speckle-dui.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "speckle-dui.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "speckle-dui.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,122 @@
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.fullname" . }}-configuration
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
data:
|
||||
configuration.toml: |
|
||||
[general]
|
||||
|
||||
#### Address & Root dir
|
||||
host = "::"
|
||||
port = 80
|
||||
root = "/app/dist"
|
||||
|
||||
#### Logging
|
||||
log-level = "info"
|
||||
|
||||
#### Cache Control headers
|
||||
cache-control-headers = true
|
||||
|
||||
#### Auto Compression
|
||||
compression = true
|
||||
compression-level = "default"
|
||||
|
||||
#### Error pages
|
||||
# Note: If a relative path is used then it will be resolved under the root directory.
|
||||
page404 = "./404.html"
|
||||
page50x = "./50x.html"
|
||||
|
||||
#### HTTP/2 + TLS
|
||||
# Note: We expect TLS termination to be handled by a reverse proxy (e.g. Nginx, Traefik, Cloudflare, etc.)
|
||||
http2 = false
|
||||
http2-tls-cert = ""
|
||||
http2-tls-key = ""
|
||||
## we are terminating https upstream; redirect is at edge proxy (ingress/gateway)
|
||||
https-redirect = false
|
||||
https-redirect-host = "localhost"
|
||||
https-redirect-from-port = 80
|
||||
https-redirect-from-hosts = "localhost"
|
||||
|
||||
#### CORS & Security headers
|
||||
## security-headers must be disabled for iframe compatibility as they include x-frame-options: deny as default
|
||||
# security-headers = false
|
||||
## cors-allows-origins is unset as iframe embedding does not require CORS, we are not fetching from another origin via XHR/fetch, and wildcard increases attack surface.
|
||||
# cors-allow-origins = ""
|
||||
|
||||
#### Directory listing
|
||||
directory-listing = false
|
||||
|
||||
#### Directory listing sorting code
|
||||
directory-listing-order = 1
|
||||
|
||||
#### Directory listing content format
|
||||
directory-listing-format = "html"
|
||||
|
||||
#### Directory listing download format
|
||||
directory-listing-download = []
|
||||
|
||||
#### File descriptor binding
|
||||
# fd = ""
|
||||
|
||||
#### Worker threads
|
||||
threads-multiplier = 1
|
||||
|
||||
#### Grace period after a graceful shutdown
|
||||
grace-period = 0
|
||||
|
||||
#### Page fallback for 404s
|
||||
# page-fallback = ""
|
||||
|
||||
#### Log request Remote Address if available
|
||||
log-remote-address = true
|
||||
|
||||
#### Log real IP from X-Forwarded-For header if available
|
||||
log-forwarded-for = true
|
||||
|
||||
#### IPs to accept the X-Forwarded-For header from. Empty means all
|
||||
trusted-proxies = {{ .Values.security.trustedProxies | toJson }}
|
||||
|
||||
#### Redirect to trailing slash in the requested directory uri
|
||||
redirect-trailing-slash = true
|
||||
|
||||
#### Check for existing pre-compressed files
|
||||
compression-static = true
|
||||
|
||||
#### Health-check endpoint (GET or HEAD `/health`)
|
||||
health = true
|
||||
|
||||
#### Markdown content negotiation
|
||||
accept-markdown = false
|
||||
|
||||
#### Maintenance Mode
|
||||
|
||||
maintenance-mode = false
|
||||
# maintenance-mode-status = 503
|
||||
# maintenance-mode-file = "./maintenance.html"
|
||||
|
||||
[advanced]
|
||||
|
||||
#### HTTP Headers customization
|
||||
[[advanced.headers]]
|
||||
source = "/*.html"
|
||||
[advanced.headers.headers]
|
||||
# Cache-Control = "public, max-age=36000"
|
||||
Content-Security-Policy = """\
|
||||
frame-ancestors {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }}; \
|
||||
default-src 'self'; \
|
||||
frame-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }}; \
|
||||
script-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} 'unsafe-inline'; \
|
||||
style-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} 'unsafe-inline'; \
|
||||
img-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} data: blob:; \
|
||||
connect-src {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }}; \
|
||||
object-src 'none'; \
|
||||
base-uri 'self'; \
|
||||
form-action {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }};\
|
||||
"""
|
||||
# Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
|
||||
X-Content-Type-Options = "nosniff"
|
||||
Referrer-Policy = "strict-origin-when-cross-origin"
|
||||
Permissions-Policy = "geolocation=(), microphone=(), camera=()"
|
||||
## Purposefully do not set X-Frame-Options as this is intended to be an iframe
|
||||
@@ -0,0 +1,84 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.fullname" . }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "speckle-dui.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "speckle-dui.serviceAccountName" . }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- with .Values.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
{{- with .Values.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80 # Needs to match port defined in deployment/docker/configuration.toml
|
||||
protocol: TCP
|
||||
{{- with .Values.livenessProbe }}
|
||||
livenessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.readinessProbe }}
|
||||
readinessProbe:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: configuration
|
||||
mountPath: /app/configuration.toml
|
||||
subPath: configuration.toml
|
||||
readOnly: true
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: configuration
|
||||
configMap:
|
||||
name: {{ include "speckle-dui.fullname" . }}-configuration
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,38 @@
|
||||
{{- if .Values.httpRoute.enabled -}}
|
||||
{{- $fullName := include "speckle-dui.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
{{- with .Values.httpRoute.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
parentRefs:
|
||||
{{- with .Values.httpRoute.parentRefs }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.httpRoute.hostnames }}
|
||||
hostnames:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.httpRoute.rules }}
|
||||
- backendRefs:
|
||||
- name: {{ $fullName }}
|
||||
port: {{ $svcPort }}
|
||||
weight: 1
|
||||
{{- with .filters }}
|
||||
filters:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .matches }}
|
||||
matches:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,43 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.fullname" . }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- with .pathType }}
|
||||
pathType: {{ . }}
|
||||
{{- end }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "speckle-dui.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.fullname" . }}
|
||||
labels: {{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector: {{- include "speckle-dui.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "speckle-dui.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "speckle-dui.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,150 @@
|
||||
# Default values for speckle-dui3.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
image:
|
||||
repository: ghcr.io/specklesystems/speckle-dui
|
||||
# This sets the pull policy for images.
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created.
|
||||
create: true
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: false
|
||||
# Annotations to add to the service account.
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template.
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
port: 80
|
||||
|
||||
security:
|
||||
## The IP addresses of trusted proxies, such as loadbalancers or WAFs, that may be forwarding traffic to the dashboards. This is important for correctly handling the X-Forwarded-For header and ensuring accurate client IP logging and security measures. Empty means all proxies are trusted, which may not be secure in production environments. We recommend setting this to the specific IP addresses of your trusted proxies.
|
||||
trustedProxies: []
|
||||
## A list of urls to be added as frame-ancestors of the Content-Security-Policy header. Empty means 'self', allowing embedding only from the same origin as the dashboards. We recommend setting this to the specific hostnames of your parent applications that will be embedding the dashboards in iframes.
|
||||
frameAncestors: []
|
||||
## A list of urls to be added as frame-src (and script-src, style-src, img-src) of the Content-Security-Policy header. Empty means 'self', allowing embedding of dashboards resources only from the same origin. We recommend setting this to the specific hostnames of Speckle DUI3.
|
||||
frameSource: []
|
||||
|
||||
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
# As dashboards expect to serve all paths under the root, we recommend using a dedicated hostname for the service, e.g. dashboards.example.com, and not sharing it with other services.
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
# Please retain this path, the dashboards expect to serve all paths under the root.
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
# -- Expose the service via gateway-api HTTPRoute
|
||||
# Requires Gateway API resources and suitable controller installed within the cluster
|
||||
# (see: https://gateway-api.sigs.k8s.io/guides/)
|
||||
httpRoute:
|
||||
# HTTPRoute enabled.
|
||||
enabled: false
|
||||
# HTTPRoute annotations.
|
||||
annotations: {}
|
||||
# Which Gateways this Route is attached to.
|
||||
parentRefs:
|
||||
- name: gateway
|
||||
sectionName: http
|
||||
# namespace: default
|
||||
# Hostnames matching HTTP header.
|
||||
hostnames:
|
||||
# As dashboards expect to serve all paths under the root, we recommend using a dedicated hostname for the service, e.g. dashboards.example.com, and not sharing it with other services.
|
||||
- chart-example.local
|
||||
# List of rules and filters applied.
|
||||
rules:
|
||||
- matches:
|
||||
# Please retain this path, the dashboards expect to serve all paths under the root.
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
@@ -0,0 +1,81 @@
|
||||
const CHALLENGE_KEY = 'speckle_challenge'
|
||||
const CHALLENGE_URL_KEY = 'speckle_url_challenge'
|
||||
const CODE_VERIFIER_KEY = 'speckle_code_verifier'
|
||||
|
||||
function toBase64Url(buffer: Uint8Array): string {
|
||||
const base64 = btoa(String.fromCharCode(...buffer))
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically random base64url-encoded string.
|
||||
* 32 bytes → 43 characters after base64url encoding (within the RFC 7636 range of 43-128).
|
||||
*/
|
||||
function createCodeVerifier(): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(32))
|
||||
return toBase64Url(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes SHA-256 hash of a string and returns it as base64url.
|
||||
* This is the PKCE code_challenge derivation from a code_verifier.
|
||||
*/
|
||||
async function computeS256Challenge(codeVerifier: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(codeVerifier)
|
||||
const digest = await crypto.subtle.digest('SHA-256', data)
|
||||
return toBase64Url(new Uint8Array(digest))
|
||||
}
|
||||
|
||||
export interface ChallengeData {
|
||||
/** The raw random string (code_verifier in PKCE terms) */
|
||||
codeVerifier: string
|
||||
/** SHA-256 base64url hash of codeVerifier (code_challenge for S256 method) */
|
||||
codeChallenge: string
|
||||
}
|
||||
|
||||
export function useAuthManager() {
|
||||
/**
|
||||
* Generates a PKCE code_verifier + code_challenge pair and persists to localStorage.
|
||||
* Used by redirect-based sign-in flows (SignInFlow) that need to
|
||||
* recover the values after the browser navigates away and back.
|
||||
*/
|
||||
const generateChallenge = async (url: string): Promise<ChallengeData> => {
|
||||
const codeVerifier = createCodeVerifier()
|
||||
const codeChallenge = await computeS256Challenge(codeVerifier)
|
||||
localStorage.setItem(CHALLENGE_KEY, codeChallenge)
|
||||
localStorage.setItem(CODE_VERIFIER_KEY, codeVerifier)
|
||||
localStorage.setItem(CHALLENGE_URL_KEY, url)
|
||||
return { codeVerifier, codeChallenge }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PKCE code_verifier + code_challenge pair without persisting to localStorage.
|
||||
* Used by flows that keep values in memory (ExchangeTokenSignInFlow)
|
||||
* so they don't overwrite the redirect flow's stored data.
|
||||
*/
|
||||
const generateLocalChallenge = async (): Promise<ChallengeData> => {
|
||||
const codeVerifier = createCodeVerifier()
|
||||
const codeChallenge = await computeS256Challenge(codeVerifier)
|
||||
return { codeVerifier, codeChallenge }
|
||||
}
|
||||
|
||||
const getChallenge = (): string | null => {
|
||||
return localStorage.getItem(CHALLENGE_KEY)
|
||||
}
|
||||
|
||||
const getCodeVerifier = (): string | null => {
|
||||
return localStorage.getItem(CODE_VERIFIER_KEY)
|
||||
}
|
||||
|
||||
const getChallengeUrl = (): string | null => {
|
||||
return localStorage.getItem(CHALLENGE_URL_KEY)
|
||||
}
|
||||
|
||||
return {
|
||||
getChallenge,
|
||||
getCodeVerifier,
|
||||
getChallengeUrl,
|
||||
generateChallenge,
|
||||
generateLocalChallenge
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { md5 } from '@speckle/shared'
|
||||
import type { Account } from '~/lib/bindings/definitions/IAccountBinding'
|
||||
|
||||
/**
|
||||
* Checks if the server supports the new /oauth/token endpoint.
|
||||
* The server exposes GET /oauth/token returning 'supported' when available.
|
||||
*/
|
||||
export async function supportsOAuthToken(serverUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(new URL('/oauth/token', serverUrl), { method: 'GET' })
|
||||
return res.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function useTokenExchange() {
|
||||
const { $accountBinding } = useNuxtApp()
|
||||
|
||||
const exchangeAccessCode = async (
|
||||
rawServerUrl: string,
|
||||
accessCode: string,
|
||||
challenge: string,
|
||||
codeVerifier?: string
|
||||
): Promise<void> => {
|
||||
// Normalize to origin (strips trailing slash, path, etc.)
|
||||
// so account IDs stay consistent with connectors
|
||||
const serverUrl = new URL(rawServerUrl).origin
|
||||
const tokenHeaders = { 'Content-Type': 'application/json' }
|
||||
let tokenResponse: Response
|
||||
|
||||
// If we have a codeVerifier, try the new PKCE-based /oauth/token endpoint first
|
||||
if (codeVerifier && (await supportsOAuthToken(serverUrl))) {
|
||||
tokenResponse = await fetch(new URL('/oauth/token', serverUrl), {
|
||||
method: 'POST',
|
||||
headers: tokenHeaders,
|
||||
body: JSON.stringify({
|
||||
appId: 'sdui',
|
||||
appSecret: 'sdui',
|
||||
accessCode,
|
||||
codeVerifier
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Fall back to legacy /auth/token with plain challenge
|
||||
tokenResponse = await fetch(new URL('/auth/token', serverUrl), {
|
||||
method: 'POST',
|
||||
headers: tokenHeaders,
|
||||
body: JSON.stringify({
|
||||
appId: 'sdui',
|
||||
appSecret: 'sdui',
|
||||
accessCode,
|
||||
challenge
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text()
|
||||
throw new Error(
|
||||
`Token exchange failed with status ${tokenResponse.status}: ${errorText}`
|
||||
)
|
||||
}
|
||||
|
||||
const { token, refreshToken } = (await tokenResponse.json()) as {
|
||||
token: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
// Query user and server info
|
||||
const graphqlQuery = {
|
||||
query:
|
||||
'query { activeUser { id name email company avatar } serverInfo { name company adminContact description version } }'
|
||||
}
|
||||
|
||||
const userAndServerInfoResponse = await fetch(new URL('/graphql', serverUrl), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(graphqlQuery)
|
||||
})
|
||||
|
||||
if (!userAndServerInfoResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch user info with status ${userAndServerInfoResponse.status}`
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const userAndServerInfo = await userAndServerInfoResponse.json()
|
||||
|
||||
const accountId = md5(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
userAndServerInfo.data.activeUser.email + serverUrl
|
||||
).toUpperCase()
|
||||
|
||||
const account: Account = {
|
||||
id: accountId,
|
||||
token,
|
||||
refreshToken,
|
||||
isDefault: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
serverInfo: { url: serverUrl, ...userAndServerInfo.data.serverInfo },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
userInfo: userAndServerInfo.data.activeUser
|
||||
}
|
||||
|
||||
await $accountBinding.addAccount(accountId, account)
|
||||
}
|
||||
|
||||
return { exchangeAccessCode }
|
||||
}
|
||||
@@ -7,7 +7,9 @@ export const IAccountBindingKey = 'accountsBinding'
|
||||
|
||||
export interface IAccountBinding extends IBinding<IAccountBindingEvents> {
|
||||
getAccounts: () => Promise<Account[]>
|
||||
addAccount: (accountId: string, account: Account) => Promise<void>
|
||||
removeAccount: (accountId: string) => Promise<void>
|
||||
authenticateAccount: (serverUrl: string) => Promise<Account>
|
||||
}
|
||||
|
||||
// An almost 1-1 mapping of what we need from the Core accounts class.
|
||||
@@ -15,6 +17,7 @@ export type Account = {
|
||||
id: string
|
||||
isDefault: boolean
|
||||
token: string
|
||||
refreshToken: string
|
||||
serverInfo: {
|
||||
name: string
|
||||
url: string
|
||||
@@ -54,6 +57,29 @@ export class MockedAccountBinding implements IAccountBinding {
|
||||
]) as Account[]
|
||||
}
|
||||
|
||||
public async addAccount(accountId: string, account: Account) {
|
||||
return await console.log('no way dude', accountId, account)
|
||||
}
|
||||
|
||||
public async authenticateAccount(serverUrl: string) {
|
||||
const config = useRuntimeConfig()
|
||||
return (await {
|
||||
id: 'whatever',
|
||||
isDefault: true,
|
||||
token: config.public.speckleToken,
|
||||
serverInfo: {
|
||||
name: 'test',
|
||||
url: serverUrl,
|
||||
frontend2: true
|
||||
},
|
||||
userInfo: {
|
||||
id: 'whatever',
|
||||
avatar: 'whatever',
|
||||
email: ''
|
||||
}
|
||||
}) as Account
|
||||
}
|
||||
|
||||
public async removeAccount(accountId: string) {
|
||||
return await console.log('no way dude', accountId)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface IBasicConnectorBinding
|
||||
highlightObjects: (objectIds: string[]) => Promise<void>
|
||||
removeModel: (model: IModelCard) => Promise<void>
|
||||
removeModels: (models: IModelCard[]) => Promise<void>
|
||||
updateParameters: (payload: string) => Promise<void>
|
||||
}
|
||||
|
||||
export interface IBasicConnectorBindingHostEvents
|
||||
@@ -107,6 +108,10 @@ export class MockedBaseBinding implements IBasicConnectorBinding {
|
||||
await console.log('no way dude')
|
||||
}
|
||||
|
||||
public async updateParameters(payload: string) {
|
||||
await console.log('Mock: updateParameters called with payload:', payload)
|
||||
}
|
||||
|
||||
public async showDevTools() {
|
||||
await console.log('No way dude')
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export type GlobalConfig = {
|
||||
|
||||
export type ConnectorConfig = {
|
||||
darkTheme: boolean
|
||||
disableCache?: boolean
|
||||
}
|
||||
|
||||
export type AccountsConfig = {
|
||||
@@ -48,7 +49,7 @@ export class MockedConfigBinding implements IConfigBinding {
|
||||
}
|
||||
|
||||
public async getConfig() {
|
||||
return await { darkTheme: false }
|
||||
return await { darkTheme: false, disableCache: false }
|
||||
}
|
||||
|
||||
public async getGlobalConfig() {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface IParametersBinding {
|
||||
update: (payload: string) => Promise<void>
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export interface ISendBindingEvents
|
||||
modelCardId: string
|
||||
versionId: string
|
||||
sendConversionResults: ConversionResult[]
|
||||
ingestionId?: string
|
||||
}) => void
|
||||
setIdMap: (args: {
|
||||
modelCardId: string
|
||||
|
||||
+69
-15
@@ -16,6 +16,9 @@ import type { Emitter } from 'nanoevents'
|
||||
import { useDesktopService } from '~/lib/core/composables/desktopService'
|
||||
import type { ToastNotification } from '@speckle/ui-components'
|
||||
import { ToastNotificationType } from '@speckle/ui-components'
|
||||
import { useModelIngestion } from '../ingestion/composables/useModelIngestion'
|
||||
import type { ISenderModelCard } from '../models/card/send'
|
||||
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
|
||||
|
||||
export type SendBatchViaBrowserArgs = {
|
||||
modelCardId: string
|
||||
@@ -466,24 +469,75 @@ export class ArchicadBridge {
|
||||
}
|
||||
|
||||
private async createVersion(args: CreateVersionArgs) {
|
||||
const accountStore = useAccountStore()
|
||||
const { accounts } = storeToRefs(accountStore)
|
||||
const account = accounts.value.find((acc) => acc.accountInfo.id === args.accountId)
|
||||
const hostAppStore = useHostAppStore()
|
||||
const { completeIngestionWithVersion } = useModelIngestion()
|
||||
const { canCreateModelIngestion } = useCheckGraphql()
|
||||
|
||||
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
|
||||
useMutation(createVersionMutation)
|
||||
const modelCard = hostAppStore.models.find(
|
||||
(model) => model.modelCardId === args.modelCardId
|
||||
) as ISenderModelCard
|
||||
|
||||
const canCreateIngestion = await canCreateModelIngestion(
|
||||
modelCard.projectId,
|
||||
modelCard.modelId,
|
||||
modelCard.accountId
|
||||
)
|
||||
|
||||
const hostAppStore = useHostAppStore()
|
||||
|
||||
const result = await createVersion.mutate({
|
||||
input: {
|
||||
modelId: args.modelId,
|
||||
objectId: args.referencedObjectId,
|
||||
sourceApplication: hostAppStore.hostAppName,
|
||||
projectId: args.projectId
|
||||
if (canCreateIngestion.queryAvailable) {
|
||||
const ingestionId = hostAppStore.activeIngestions[args.modelCardId]
|
||||
if (!ingestionId) {
|
||||
hostAppStore.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Ingestion Failed',
|
||||
description: 'Ingestion ID not found to create version.'
|
||||
})
|
||||
throw new Error(`Ingestion failed: Ingestion ID not found to create version.`)
|
||||
}
|
||||
})
|
||||
return result?.data?.versionMutations?.create?.id
|
||||
|
||||
const res = await completeIngestionWithVersion(
|
||||
modelCard,
|
||||
ingestionId,
|
||||
args.referencedObjectId
|
||||
)
|
||||
|
||||
if (res?.statusData.__typename === 'ModelIngestionSuccessStatus') {
|
||||
return res?.statusData.versionId
|
||||
}
|
||||
|
||||
if (res?.statusData.__typename === 'ModelIngestionFailedStatus') {
|
||||
const errorReason = res?.statusData.errorReason || 'Unknown error'
|
||||
hostAppStore.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Ingestion Failed',
|
||||
description: errorReason
|
||||
})
|
||||
throw new Error(`Ingestion failed: ${errorReason}.`)
|
||||
}
|
||||
|
||||
hostAppStore.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Ingestion Error',
|
||||
description: 'Ingestion status does not match expected types.'
|
||||
})
|
||||
throw new Error(
|
||||
`Ingestion status does not match with the expected types as success or failure.`
|
||||
)
|
||||
} else {
|
||||
const accountStore = useAccountStore()
|
||||
const account = accountStore.getAccountClient(args.accountId)
|
||||
const { mutate } = provideApolloClient(account)(() =>
|
||||
useMutation(createVersionMutation)
|
||||
)
|
||||
|
||||
const result = await mutate({
|
||||
input: {
|
||||
modelId: args.modelId,
|
||||
objectId: args.referencedObjectId,
|
||||
sourceApplication: args.sourceApplication || 'Archicad',
|
||||
projectId: args.projectId
|
||||
}
|
||||
})
|
||||
return result?.data?.versionMutations?.create?.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+79
-25
@@ -19,6 +19,9 @@ import type {
|
||||
ReceiveViaBrowserArgs,
|
||||
CreateVersionArgs
|
||||
} from '~/lib/bridge/server'
|
||||
import { useModelIngestion } from '../ingestion/composables/useModelIngestion'
|
||||
import type { ISenderModelCard } from '../models/card/send'
|
||||
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
|
||||
|
||||
declare let sketchup: {
|
||||
exec: (data: Record<string, unknown>) => void
|
||||
@@ -127,15 +130,17 @@ export class SketchupBridge extends BaseBridge {
|
||||
objectId: result.data.project.model.version.referencedObject as string
|
||||
})
|
||||
|
||||
const updateProgress = (e: {
|
||||
const updateProgress = (_: {
|
||||
stage: ProgressStage
|
||||
current: number
|
||||
total: number
|
||||
}) => {
|
||||
const progress = e.current / e.total
|
||||
// TODO: replace object loader with loader 2, for now progress is not return total and it end up with infinity
|
||||
// const progress = e.current / e.total
|
||||
|
||||
hostAppStore.handleModelProgressEvents({
|
||||
modelCardId: eventPayload.modelCardId,
|
||||
progress: { status: 'Downloading', progress }
|
||||
progress: { status: 'Downloading' }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,40 +300,89 @@ export class SketchupBridge extends BaseBridge {
|
||||
sourceApplication: 'sketchup',
|
||||
message: message || 'send from sketchup'
|
||||
}
|
||||
const versionId = await this.createVersion(args)
|
||||
|
||||
const hostAppStore = useHostAppStore()
|
||||
// TODO: Alignment needed
|
||||
hostAppStore.setModelSendResult({
|
||||
modelCardId: args.modelCardId,
|
||||
versionId: versionId as string,
|
||||
sendConversionResults
|
||||
})
|
||||
try {
|
||||
const versionId = await this.createVersion(args)
|
||||
hostAppStore.setModelSendResult({
|
||||
modelCardId: args.modelCardId,
|
||||
versionId: versionId as string,
|
||||
sendConversionResults
|
||||
})
|
||||
} catch (err) {
|
||||
hostAppStore.setHostAppError({
|
||||
message: (err as Error).message || 'Unknown error occurred',
|
||||
error: (err as Error).toString(),
|
||||
stackTrace: (err as Error).stack || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async createVersion(args: CreateVersionArgs) {
|
||||
const accountStore = useAccountStore()
|
||||
const hostAppStore = useHostAppStore()
|
||||
const accountStore = useAccountStore()
|
||||
const { accounts } = storeToRefs(accountStore)
|
||||
const account = accounts.value.find((acc) => acc.accountInfo.id === args.accountId)
|
||||
const { completeIngestionWithVersion } = useModelIngestion()
|
||||
|
||||
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
|
||||
useMutation(createVersionMutation)
|
||||
const modelCard = hostAppStore.models.find(
|
||||
(model) => model.modelCardId === args.modelCardId
|
||||
)
|
||||
|
||||
// sketchup versions are provided as 2 digit. i.e. 22, 23, 24
|
||||
// we are safe with this string concatanation for 77 years
|
||||
const hostAppName = `SketchUp 20${hostAppStore.hostAppVersion}`
|
||||
if (!modelCard) {
|
||||
throw new Error('Model card not found') // ctor
|
||||
}
|
||||
|
||||
const result = await createVersion.mutate({
|
||||
input: {
|
||||
modelId: args.modelId,
|
||||
objectId: args.referencedObjectId,
|
||||
sourceApplication: hostAppName,
|
||||
projectId: args.projectId
|
||||
const { canCreateModelIngestion } = useCheckGraphql()
|
||||
const canCreateIngestion = await canCreateModelIngestion(
|
||||
modelCard.projectId,
|
||||
modelCard.modelId,
|
||||
modelCard.accountId
|
||||
)
|
||||
|
||||
if (canCreateIngestion.queryAvailable) {
|
||||
const ingestionId = hostAppStore.activeIngestions[args.modelCardId]
|
||||
if (!ingestionId) {
|
||||
throw new Error(`Ingestion failed: Ingestion ID not found to create version.`)
|
||||
}
|
||||
})
|
||||
return result?.data?.versionMutations?.create?.id
|
||||
|
||||
const res = await completeIngestionWithVersion(
|
||||
modelCard as ISenderModelCard,
|
||||
ingestionId,
|
||||
args.referencedObjectId
|
||||
)
|
||||
|
||||
if (res?.statusData.__typename === 'ModelIngestionSuccessStatus') {
|
||||
return res?.statusData.versionId
|
||||
}
|
||||
|
||||
if (res?.statusData.__typename === 'ModelIngestionFailedStatus') {
|
||||
throw new Error(
|
||||
`Ingestion failed: ${res?.statusData.errorReason || 'Unknown error'}.`
|
||||
)
|
||||
}
|
||||
throw new Error(
|
||||
`Ingestion status does not match with the expected types as success or failure.`
|
||||
)
|
||||
} else {
|
||||
// for the self hosters that does not have available graphql for ingestions
|
||||
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
|
||||
useMutation(createVersionMutation)
|
||||
)
|
||||
|
||||
// sketchup versions are provided as 2 digit. i.e. 22, 23, 24
|
||||
// we are safe with this string concatanation for 77 years
|
||||
const hostAppName = `SketchUp 20${hostAppStore.hostAppVersion}`
|
||||
|
||||
const result = await createVersion.mutate({
|
||||
input: {
|
||||
modelId: args.modelId,
|
||||
objectId: args.referencedObjectId,
|
||||
sourceApplication: hostAppName,
|
||||
projectId: args.projectId
|
||||
}
|
||||
})
|
||||
return result?.data?.versionMutations?.create?.id
|
||||
}
|
||||
}
|
||||
|
||||
public async create(): Promise<boolean> {
|
||||
|
||||
@@ -22,7 +22,7 @@ type Documents = {
|
||||
"\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectListProjectItem\n }\n }\n }\n": typeof types.CreateProjectDocument,
|
||||
"\n mutation CreateProjectInWorkspace($input: WorkspaceProjectCreateInput!) {\n workspaceMutations {\n projects {\n create(input: $input) {\n ...ProjectListProjectItem\n }\n }\n }\n }\n": typeof types.CreateProjectInWorkspaceDocument,
|
||||
"\n mutation StreamAccessRequestCreate($input: String!) {\n streamAccessRequestCreate(streamId: $input) {\n id\n }\n }\n": typeof types.StreamAccessRequestCreateDocument,
|
||||
"\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logo\n role\n readOnly\n creationState {\n completed\n }\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n": typeof types.WorkspaceListWorkspaceItemFragmentDoc,
|
||||
"\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n": typeof types.WorkspaceListWorkspaceItemFragmentDoc,
|
||||
"\n fragment AutomateFunctionItem on AutomateFunction {\n name\n isFeatured\n id\n creator {\n name\n }\n releases {\n items {\n inputSchema\n }\n }\n }\n": typeof types.AutomateFunctionItemFragmentDoc,
|
||||
"\n mutation CreateAutomation($projectId: ID!, $input: ProjectAutomationCreateInput!) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n create(input: $input) {\n id\n name\n }\n }\n }\n }\n": typeof types.CreateAutomationDocument,
|
||||
"\n fragment AutomateFunctionRunItem on AutomateFunctionRun {\n id\n status\n statusMessage\n results\n contextView\n function {\n id\n name\n logo\n }\n }\n": typeof types.AutomateFunctionRunItemFragmentDoc,
|
||||
@@ -33,6 +33,7 @@ type Documents = {
|
||||
"\n query CanCreatePersonalProject {\n activeUser {\n permissions {\n canCreatePersonalProject {\n authorized\n code\n message\n payload\n }\n }\n }\n }\n": typeof types.CanCreatePersonalProjectDocument,
|
||||
"\n query CanCreateProjectInWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n permissions {\n canCreateProject {\n authorized\n code\n message\n payload\n }\n }\n }\n }\n": typeof types.CanCreateProjectInWorkspaceDocument,
|
||||
"\n query CanCreateModelInProject($projectId: String!) {\n project(id: $projectId) {\n permissions {\n canCreateModel {\n authorized\n code\n message\n }\n }\n }\n }\n": typeof types.CanCreateModelInProjectDocument,
|
||||
"\n query CanCreateVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateVersion {\n authorized\n code\n message\n errorMessage\n }\n }\n }\n }\n }\n": typeof types.CanCreateVersionDocument,
|
||||
"\n query ActiveWorkspace {\n activeUser {\n activeWorkspace {\n id\n name\n }\n }\n }\n": typeof types.ActiveWorkspaceDocument,
|
||||
"\n fragment ProjectListProjectItem on Project {\n id\n name\n role\n updatedAt\n workspaceId\n workspace {\n id\n name\n slug\n role\n }\n models {\n totalCount\n }\n permissions {\n canLoad {\n authorized\n code\n message\n }\n canPublish {\n authorized\n code\n message\n }\n }\n }\n": typeof types.ProjectListProjectItemFragmentDoc,
|
||||
"\n query ProjectListQuery($limit: Int!, $filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(limit: $limit, filter: $filter, cursor: $cursor) {\n totalCount\n cursor\n items {\n ...ProjectListProjectItem\n }\n }\n }\n }\n": typeof types.ProjectListQueryDocument,
|
||||
@@ -44,7 +45,7 @@ type Documents = {
|
||||
"\n query ProjectAddByUrlQueryWithVersion(\n $projectId: String!\n $modelId: String!\n $versionId: String!\n ) {\n project(id: $projectId) {\n ...ProjectListProjectItem\n model(id: $modelId) {\n ...ModelListModelItem\n version(id: $versionId) {\n ...VersionListItem\n }\n }\n }\n }\n": typeof types.ProjectAddByUrlQueryWithVersionDocument,
|
||||
"\n query ProjectAddByUrlQueryWithoutVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n ...ProjectListProjectItem\n model(id: $modelId) {\n ...ModelListModelItem\n }\n }\n }\n": typeof types.ProjectAddByUrlQueryWithoutVersionDocument,
|
||||
"\n query ProjectDetails($projectId: String!) {\n project(id: $projectId) {\n id\n role\n name\n workspace {\n name\n slug\n readOnly\n role\n }\n team {\n user {\n avatar\n id\n name\n }\n }\n visibility\n permissions {\n canLoad {\n authorized\n code\n message\n }\n canPublish {\n authorized\n code\n message\n }\n }\n }\n }\n": typeof types.ProjectDetailsDocument,
|
||||
"\n query AutomateFunctions {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n": typeof types.AutomateFunctionsDocument,
|
||||
"\n query AutomateFunctions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n }\n": typeof types.AutomateFunctionsDocument,
|
||||
"\n query ModelDetails($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n id\n name\n model(id: $modelId) {\n id\n displayName\n name\n versions {\n totalCount\n items {\n id\n }\n }\n author {\n id\n name\n avatar\n }\n }\n }\n }\n": typeof types.ModelDetailsDocument,
|
||||
"\n query VersionDetails($projectId: String!, $versionId: String!, $modelId: String!) {\n project(id: $projectId) {\n id\n name\n model(id: $modelId) {\n id\n name\n versions(limit: 1) {\n items {\n id\n createdAt\n sourceApplication\n authorUser {\n id\n }\n }\n }\n version(id: $versionId) {\n id\n referencedObject\n message\n sourceApplication\n createdAt\n previewUrl\n }\n }\n }\n }\n": typeof types.VersionDetailsDocument,
|
||||
"\n query ServerInfo {\n serverInfo {\n workspaces {\n workspacesEnabled\n }\n }\n }\n": typeof types.ServerInfoDocument,
|
||||
@@ -52,8 +53,19 @@ type Documents = {
|
||||
"\n subscription ProjectTriggeredAutomationsStatusUpdated($projectId: String!) {\n projectTriggeredAutomationsStatusUpdated(projectId: $projectId) {\n type\n version {\n id\n }\n model {\n id\n }\n project {\n id\n }\n run {\n ...AutomationRunItem\n }\n }\n }\n": typeof types.ProjectTriggeredAutomationsStatusUpdatedDocument,
|
||||
"\n subscription OnUserProjectsUpdated {\n userProjectsUpdated {\n id\n project {\n id\n visibility\n team {\n id\n role\n }\n }\n }\n }\n": typeof types.OnUserProjectsUpdatedDocument,
|
||||
"\n subscription ProjectUpdated($projectId: String!) {\n projectUpdated(id: $projectId) {\n id\n project {\n visibility\n }\n }\n }\n": typeof types.ProjectUpdatedDocument,
|
||||
"\n subscription Subscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n": typeof types.SubscriptionDocument,
|
||||
"\n subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n": typeof types.ModelViewingSubscriptionDocument,
|
||||
"\n subscription ProjectCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n comment {\n author {\n avatar\n id\n name\n }\n id\n hasParent\n parent {\n id\n }\n }\n type\n }\n }\n": typeof types.ProjectCommentsUpdatedDocument,
|
||||
"\n mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {\n projectMutations {\n modelIngestionMutations {\n create(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.CreateModelIngestionDocument,
|
||||
"\n mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {\n projectMutations {\n modelIngestionMutations {\n updateProgress(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.UpdateModelIngestionProgressDocument,
|
||||
"\n mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {\n projectMutations {\n modelIngestionMutations {\n completeWithVersion(input: $input) {\n id\n statusData {\n __typename\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionFailedStatus {\n errorStacktrace\n errorReason\n status\n }\n ... on ModelIngestionCancelledStatus {\n cancellationMessage\n status\n }\n ... on ModelIngestionQueuedStatus {\n progressMessage\n status\n }\n }\n }\n }\n }\n }\n": typeof types.CompleteModelIngestionWithVersionDocument,
|
||||
"\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.FailModelIngestionWithErrorDocument,
|
||||
"\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.FailModelIngestionWithCancelDocument,
|
||||
"\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n": typeof types.CanCreateIngestionDocument,
|
||||
"\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n": typeof types.ProjectModelIngestionUpdatedDocument,
|
||||
"\n fragment IssuesItem on Issue {\n id\n status\n title\n priority\n viewerState\n identifier\n resourceIdString\n activities(input: { limit: 1, sortDirection: asc }) {\n totalCount\n items {\n actor {\n id\n user {\n name\n id\n avatar\n }\n }\n eventType\n createdAt\n }\n }\n replies {\n totalCount\n items {\n id\n author {\n id\n user {\n name\n id\n avatar\n }\n }\n createdAt\n description {\n doc\n }\n }\n }\n description {\n doc\n }\n labels {\n hexColor\n id\n name\n }\n author {\n id\n user {\n id\n name\n avatar\n }\n }\n dueDate\n assignee {\n id\n user {\n id\n avatar\n name\n }\n }\n }\n": typeof types.IssuesItemFragmentDoc,
|
||||
"\n query IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n": typeof types.IssuesListDocument,
|
||||
"\n query IssueResourceMetaSearch(\n $workspaceId: String!\n $resourceType: ResourceMetaType!\n $resourceId: String!\n $projectId: String\n $metaType: String\n ) {\n resourceMetaSearch(\n workspaceId: $workspaceId\n resourceType: $resourceType\n resourceId: $resourceId\n projectId: $projectId\n metaType: $metaType\n ) {\n data\n }\n }\n": typeof types.IssueResourceMetaSearchDocument,
|
||||
"\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n": typeof types.WorkspacePlanUsageUpdatedDocument,
|
||||
};
|
||||
const documents: Documents = {
|
||||
"\n mutation SetActiveWorkspaceMutation($slug: String) {\n activeUserMutations {\n setActiveWorkspace(slug: $slug) {\n id\n }\n }\n }\n": types.SetActiveWorkspaceMutationDocument,
|
||||
@@ -64,7 +76,7 @@ const documents: Documents = {
|
||||
"\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectListProjectItem\n }\n }\n }\n": types.CreateProjectDocument,
|
||||
"\n mutation CreateProjectInWorkspace($input: WorkspaceProjectCreateInput!) {\n workspaceMutations {\n projects {\n create(input: $input) {\n ...ProjectListProjectItem\n }\n }\n }\n }\n": types.CreateProjectInWorkspaceDocument,
|
||||
"\n mutation StreamAccessRequestCreate($input: String!) {\n streamAccessRequestCreate(streamId: $input) {\n id\n }\n }\n": types.StreamAccessRequestCreateDocument,
|
||||
"\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logo\n role\n readOnly\n creationState {\n completed\n }\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n": types.WorkspaceListWorkspaceItemFragmentDoc,
|
||||
"\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n": types.WorkspaceListWorkspaceItemFragmentDoc,
|
||||
"\n fragment AutomateFunctionItem on AutomateFunction {\n name\n isFeatured\n id\n creator {\n name\n }\n releases {\n items {\n inputSchema\n }\n }\n }\n": types.AutomateFunctionItemFragmentDoc,
|
||||
"\n mutation CreateAutomation($projectId: ID!, $input: ProjectAutomationCreateInput!) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n create(input: $input) {\n id\n name\n }\n }\n }\n }\n": types.CreateAutomationDocument,
|
||||
"\n fragment AutomateFunctionRunItem on AutomateFunctionRun {\n id\n status\n statusMessage\n results\n contextView\n function {\n id\n name\n logo\n }\n }\n": types.AutomateFunctionRunItemFragmentDoc,
|
||||
@@ -75,6 +87,7 @@ const documents: Documents = {
|
||||
"\n query CanCreatePersonalProject {\n activeUser {\n permissions {\n canCreatePersonalProject {\n authorized\n code\n message\n payload\n }\n }\n }\n }\n": types.CanCreatePersonalProjectDocument,
|
||||
"\n query CanCreateProjectInWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n permissions {\n canCreateProject {\n authorized\n code\n message\n payload\n }\n }\n }\n }\n": types.CanCreateProjectInWorkspaceDocument,
|
||||
"\n query CanCreateModelInProject($projectId: String!) {\n project(id: $projectId) {\n permissions {\n canCreateModel {\n authorized\n code\n message\n }\n }\n }\n }\n": types.CanCreateModelInProjectDocument,
|
||||
"\n query CanCreateVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateVersion {\n authorized\n code\n message\n errorMessage\n }\n }\n }\n }\n }\n": types.CanCreateVersionDocument,
|
||||
"\n query ActiveWorkspace {\n activeUser {\n activeWorkspace {\n id\n name\n }\n }\n }\n": types.ActiveWorkspaceDocument,
|
||||
"\n fragment ProjectListProjectItem on Project {\n id\n name\n role\n updatedAt\n workspaceId\n workspace {\n id\n name\n slug\n role\n }\n models {\n totalCount\n }\n permissions {\n canLoad {\n authorized\n code\n message\n }\n canPublish {\n authorized\n code\n message\n }\n }\n }\n": types.ProjectListProjectItemFragmentDoc,
|
||||
"\n query ProjectListQuery($limit: Int!, $filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(limit: $limit, filter: $filter, cursor: $cursor) {\n totalCount\n cursor\n items {\n ...ProjectListProjectItem\n }\n }\n }\n }\n": types.ProjectListQueryDocument,
|
||||
@@ -86,7 +99,7 @@ const documents: Documents = {
|
||||
"\n query ProjectAddByUrlQueryWithVersion(\n $projectId: String!\n $modelId: String!\n $versionId: String!\n ) {\n project(id: $projectId) {\n ...ProjectListProjectItem\n model(id: $modelId) {\n ...ModelListModelItem\n version(id: $versionId) {\n ...VersionListItem\n }\n }\n }\n }\n": types.ProjectAddByUrlQueryWithVersionDocument,
|
||||
"\n query ProjectAddByUrlQueryWithoutVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n ...ProjectListProjectItem\n model(id: $modelId) {\n ...ModelListModelItem\n }\n }\n }\n": types.ProjectAddByUrlQueryWithoutVersionDocument,
|
||||
"\n query ProjectDetails($projectId: String!) {\n project(id: $projectId) {\n id\n role\n name\n workspace {\n name\n slug\n readOnly\n role\n }\n team {\n user {\n avatar\n id\n name\n }\n }\n visibility\n permissions {\n canLoad {\n authorized\n code\n message\n }\n canPublish {\n authorized\n code\n message\n }\n }\n }\n }\n": types.ProjectDetailsDocument,
|
||||
"\n query AutomateFunctions {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n": types.AutomateFunctionsDocument,
|
||||
"\n query AutomateFunctions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n }\n": types.AutomateFunctionsDocument,
|
||||
"\n query ModelDetails($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n id\n name\n model(id: $modelId) {\n id\n displayName\n name\n versions {\n totalCount\n items {\n id\n }\n }\n author {\n id\n name\n avatar\n }\n }\n }\n }\n": types.ModelDetailsDocument,
|
||||
"\n query VersionDetails($projectId: String!, $versionId: String!, $modelId: String!) {\n project(id: $projectId) {\n id\n name\n model(id: $modelId) {\n id\n name\n versions(limit: 1) {\n items {\n id\n createdAt\n sourceApplication\n authorUser {\n id\n }\n }\n }\n version(id: $versionId) {\n id\n referencedObject\n message\n sourceApplication\n createdAt\n previewUrl\n }\n }\n }\n }\n": types.VersionDetailsDocument,
|
||||
"\n query ServerInfo {\n serverInfo {\n workspaces {\n workspacesEnabled\n }\n }\n }\n": types.ServerInfoDocument,
|
||||
@@ -94,8 +107,19 @@ const documents: Documents = {
|
||||
"\n subscription ProjectTriggeredAutomationsStatusUpdated($projectId: String!) {\n projectTriggeredAutomationsStatusUpdated(projectId: $projectId) {\n type\n version {\n id\n }\n model {\n id\n }\n project {\n id\n }\n run {\n ...AutomationRunItem\n }\n }\n }\n": types.ProjectTriggeredAutomationsStatusUpdatedDocument,
|
||||
"\n subscription OnUserProjectsUpdated {\n userProjectsUpdated {\n id\n project {\n id\n visibility\n team {\n id\n role\n }\n }\n }\n }\n": types.OnUserProjectsUpdatedDocument,
|
||||
"\n subscription ProjectUpdated($projectId: String!) {\n projectUpdated(id: $projectId) {\n id\n project {\n visibility\n }\n }\n }\n": types.ProjectUpdatedDocument,
|
||||
"\n subscription Subscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n": types.SubscriptionDocument,
|
||||
"\n subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n": types.ModelViewingSubscriptionDocument,
|
||||
"\n subscription ProjectCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n comment {\n author {\n avatar\n id\n name\n }\n id\n hasParent\n parent {\n id\n }\n }\n type\n }\n }\n": types.ProjectCommentsUpdatedDocument,
|
||||
"\n mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {\n projectMutations {\n modelIngestionMutations {\n create(input: $input) {\n id\n }\n }\n }\n }\n": types.CreateModelIngestionDocument,
|
||||
"\n mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {\n projectMutations {\n modelIngestionMutations {\n updateProgress(input: $input) {\n id\n }\n }\n }\n }\n": types.UpdateModelIngestionProgressDocument,
|
||||
"\n mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {\n projectMutations {\n modelIngestionMutations {\n completeWithVersion(input: $input) {\n id\n statusData {\n __typename\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionFailedStatus {\n errorStacktrace\n errorReason\n status\n }\n ... on ModelIngestionCancelledStatus {\n cancellationMessage\n status\n }\n ... on ModelIngestionQueuedStatus {\n progressMessage\n status\n }\n }\n }\n }\n }\n }\n": types.CompleteModelIngestionWithVersionDocument,
|
||||
"\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n": types.FailModelIngestionWithErrorDocument,
|
||||
"\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n": types.FailModelIngestionWithCancelDocument,
|
||||
"\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n": types.CanCreateIngestionDocument,
|
||||
"\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n": types.ProjectModelIngestionUpdatedDocument,
|
||||
"\n fragment IssuesItem on Issue {\n id\n status\n title\n priority\n viewerState\n identifier\n resourceIdString\n activities(input: { limit: 1, sortDirection: asc }) {\n totalCount\n items {\n actor {\n id\n user {\n name\n id\n avatar\n }\n }\n eventType\n createdAt\n }\n }\n replies {\n totalCount\n items {\n id\n author {\n id\n user {\n name\n id\n avatar\n }\n }\n createdAt\n description {\n doc\n }\n }\n }\n description {\n doc\n }\n labels {\n hexColor\n id\n name\n }\n author {\n id\n user {\n id\n name\n avatar\n }\n }\n dueDate\n assignee {\n id\n user {\n id\n avatar\n name\n }\n }\n }\n": types.IssuesItemFragmentDoc,
|
||||
"\n query IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n": types.IssuesListDocument,
|
||||
"\n query IssueResourceMetaSearch(\n $workspaceId: String!\n $resourceType: ResourceMetaType!\n $resourceId: String!\n $projectId: String\n $metaType: String\n ) {\n resourceMetaSearch(\n workspaceId: $workspaceId\n resourceType: $resourceType\n resourceId: $resourceId\n projectId: $projectId\n metaType: $metaType\n ) {\n data\n }\n }\n": types.IssueResourceMetaSearchDocument,
|
||||
"\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n": types.WorkspacePlanUsageUpdatedDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -147,7 +171,7 @@ export function graphql(source: "\n mutation StreamAccessRequestCreate($input:
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logo\n role\n readOnly\n creationState {\n completed\n }\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logo\n role\n readOnly\n creationState {\n completed\n }\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -188,6 +212,10 @@ export function graphql(source: "\n query CanCreateProjectInWorkspace($workspac
|
||||
* 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 CanCreateModelInProject($projectId: String!) {\n project(id: $projectId) {\n permissions {\n canCreateModel {\n authorized\n code\n message\n }\n }\n }\n }\n"): (typeof documents)["\n query CanCreateModelInProject($projectId: String!) {\n project(id: $projectId) {\n permissions {\n canCreateModel {\n authorized\n code\n message\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* 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 CanCreateVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateVersion {\n authorized\n code\n message\n errorMessage\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query CanCreateVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateVersion {\n authorized\n code\n message\n errorMessage\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -235,7 +263,7 @@ export function graphql(source: "\n query ProjectDetails($projectId: String!) {
|
||||
/**
|
||||
* 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 AutomateFunctions {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n"): (typeof documents)["\n query AutomateFunctions {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query AutomateFunctions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n }\n"): (typeof documents)["\n query AutomateFunctions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -267,11 +295,55 @@ export function graphql(source: "\n subscription ProjectUpdated($projectId: Str
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n subscription Subscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n"): (typeof documents)["\n subscription Subscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n"];
|
||||
export function graphql(source: "\n subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n"): (typeof documents)["\n subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n subscription ProjectCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n comment {\n author {\n avatar\n id\n name\n }\n id\n hasParent\n parent {\n id\n }\n }\n type\n }\n }\n"): (typeof documents)["\n subscription ProjectCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n comment {\n author {\n avatar\n id\n name\n }\n id\n hasParent\n parent {\n id\n }\n }\n type\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {\n projectMutations {\n modelIngestionMutations {\n create(input: $input) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {\n projectMutations {\n modelIngestionMutations {\n create(input: $input) {\n id\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {\n projectMutations {\n modelIngestionMutations {\n updateProgress(input: $input) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {\n projectMutations {\n modelIngestionMutations {\n updateProgress(input: $input) {\n id\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {\n projectMutations {\n modelIngestionMutations {\n completeWithVersion(input: $input) {\n id\n statusData {\n __typename\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionFailedStatus {\n errorStacktrace\n errorReason\n status\n }\n ... on ModelIngestionCancelledStatus {\n cancellationMessage\n status\n }\n ... on ModelIngestionQueuedStatus {\n progressMessage\n status\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {\n projectMutations {\n modelIngestionMutations {\n completeWithVersion(input: $input) {\n id\n statusData {\n __typename\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionFailedStatus {\n errorStacktrace\n errorReason\n status\n }\n ... on ModelIngestionCancelledStatus {\n cancellationMessage\n status\n }\n ... on ModelIngestionQueuedStatus {\n progressMessage\n status\n }\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* 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 CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n"): (typeof documents)["\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment IssuesItem on Issue {\n id\n status\n title\n priority\n viewerState\n identifier\n resourceIdString\n activities(input: { limit: 1, sortDirection: asc }) {\n totalCount\n items {\n actor {\n id\n user {\n name\n id\n avatar\n }\n }\n eventType\n createdAt\n }\n }\n replies {\n totalCount\n items {\n id\n author {\n id\n user {\n name\n id\n avatar\n }\n }\n createdAt\n description {\n doc\n }\n }\n }\n description {\n doc\n }\n labels {\n hexColor\n id\n name\n }\n author {\n id\n user {\n id\n name\n avatar\n }\n }\n dueDate\n assignee {\n id\n user {\n id\n avatar\n name\n }\n }\n }\n"): (typeof documents)["\n fragment IssuesItem on Issue {\n id\n status\n title\n priority\n viewerState\n identifier\n resourceIdString\n activities(input: { limit: 1, sortDirection: asc }) {\n totalCount\n items {\n actor {\n id\n user {\n name\n id\n avatar\n }\n }\n eventType\n createdAt\n }\n }\n replies {\n totalCount\n items {\n id\n author {\n id\n user {\n name\n id\n avatar\n }\n }\n createdAt\n description {\n doc\n }\n }\n }\n description {\n doc\n }\n labels {\n hexColor\n id\n name\n }\n author {\n id\n user {\n id\n name\n avatar\n }\n }\n dueDate\n assignee {\n id\n user {\n id\n avatar\n name\n }\n }\n }\n"];
|
||||
/**
|
||||
* 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 IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n"): (typeof documents)["\n query IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* 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 IssueResourceMetaSearch(\n $workspaceId: String!\n $resourceType: ResourceMetaType!\n $resourceId: String!\n $projectId: String\n $metaType: String\n ) {\n resourceMetaSearch(\n workspaceId: $workspaceId\n resourceType: $resourceType\n resourceId: $resourceId\n projectId: $projectId\n metaType: $metaType\n ) {\n data\n }\n }\n"): (typeof documents)["\n query IssueResourceMetaSearch(\n $workspaceId: String!\n $resourceType: ResourceMetaType!\n $resourceId: String!\n $projectId: String\n $metaType: String\n ) {\n resourceMetaSearch(\n workspaceId: $workspaceId\n resourceType: $resourceType\n resourceId: $resourceId\n projectId: $projectId\n metaType: $metaType\n ) {\n data\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n"): (typeof documents)["\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
||||
+2945
-509
File diff suppressed because one or more lines are too long
@@ -6,16 +6,17 @@ interface CustomProperties {
|
||||
[key: string]: object | string | boolean | number | undefined | null
|
||||
}
|
||||
|
||||
// Cached email and server
|
||||
// Cached email, server, and userId
|
||||
const lastEmail: Ref<string | undefined> = ref(undefined)
|
||||
const lastServer: Ref<string | undefined> = ref(undefined)
|
||||
const lastUserId: Ref<string | undefined> = ref(undefined)
|
||||
|
||||
/**
|
||||
* Get Mixpanel functions
|
||||
* In DUI3, quite likely to change distinct id of the track operation since we can trigger repetitive calls that belongs to different account.
|
||||
* Also we have some operations that explicitly not belong to any account, i.e. first "Send" or "Load" click,
|
||||
* with this case we use default account on manager to get "email" and "server" and cache them for later anonymous track.
|
||||
* In each call we update "lastEmail" and "lastServer" for the following potential anonymous tracks.
|
||||
* with this case we use default account on manager to get "email", "server", and "userId" and cache them for later anonymous track.
|
||||
* In each call we update "lastEmail", "lastServer", and "lastUserId" for the following potential anonymous tracks.
|
||||
*/
|
||||
export function useMixpanel() {
|
||||
const hostApp = useHostAppStore()
|
||||
@@ -42,11 +43,13 @@ export function useMixpanel() {
|
||||
const account = accounts.find((a) => a.accountInfo.id === accountId)
|
||||
lastEmail.value = account?.accountInfo.userInfo.email
|
||||
lastServer.value = account?.accountInfo.serverInfo.url
|
||||
lastUserId.value = account?.accountInfo.userInfo.id
|
||||
} else {
|
||||
// do not set if they cached already
|
||||
if (lastEmail.value === undefined || lastServer.value === undefined) {
|
||||
lastEmail.value = activeAccount.accountInfo.userInfo.email
|
||||
lastServer.value = activeAccount.accountInfo.serverInfo.url
|
||||
lastUserId.value = activeAccount.accountInfo.userInfo.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +65,9 @@ export function useMixpanel() {
|
||||
}
|
||||
const hashedEmail =
|
||||
'@' + md5(lastEmail.value.toLowerCase() as string).toUpperCase()
|
||||
const hashedServer = md5(
|
||||
new URL(lastServer.value).hostname.toLowerCase() as string
|
||||
).toUpperCase()
|
||||
const serverUrl = new URL(lastServer.value)
|
||||
const serverHostname = serverUrl.hostname.toLowerCase()
|
||||
const hashedServer = md5(serverHostname).toUpperCase()
|
||||
|
||||
// Get os info from userAgent text
|
||||
// taken from original mixpanel implementation
|
||||
@@ -84,6 +87,8 @@ export function useMixpanel() {
|
||||
distinct_id: hashedEmail,
|
||||
// eslint-disable-next-line camelcase
|
||||
server_id: hashedServer,
|
||||
// eslint-disable-next-line camelcase
|
||||
server_domain: serverHostname,
|
||||
token: mixpanelTokenId as string,
|
||||
type: isAction ? 'action' : undefined,
|
||||
hostApp: hostApp.hostAppName,
|
||||
@@ -92,6 +97,7 @@ export function useMixpanel() {
|
||||
// eslint-disable-next-line camelcase
|
||||
core_version: hostApp.connectorVersion,
|
||||
email: lastEmail.value,
|
||||
userId: lastUserId.value,
|
||||
...customProperties
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||
import type { CardSetting } from '~/lib/models/card/setting'
|
||||
|
||||
export function useSettingsTracking() {
|
||||
const { trackEvent } = useMixpanel()
|
||||
|
||||
function trackSettingsChange(
|
||||
eventName: string,
|
||||
settings: CardSetting[],
|
||||
defaultSettings: CardSetting[],
|
||||
accountId?: string,
|
||||
requireChanges: boolean = false
|
||||
) {
|
||||
// building dynamic properties
|
||||
// since this can change based on HostApp
|
||||
const settingProperties: Record<string, string | boolean | number> = {
|
||||
name: eventName
|
||||
}
|
||||
|
||||
let hasAnyChange = false
|
||||
settings.forEach((setting) => {
|
||||
const defaultSetting = defaultSettings.find((s) => s.id === setting.id)
|
||||
if (defaultSetting) {
|
||||
const isDefault = setting.value === defaultSetting.value
|
||||
if (!isDefault) {
|
||||
hasAnyChange = true
|
||||
}
|
||||
// if user selects default, just use 'default'
|
||||
settingProperties['setting_' + setting.id] = isDefault
|
||||
? `${setting.value} (default)`
|
||||
: setting.value
|
||||
}
|
||||
})
|
||||
|
||||
// only track if user changed a setting
|
||||
if (!requireChanges || hasAnyChange) {
|
||||
void trackEvent('DUI3 Action', settingProperties, accountId)
|
||||
}
|
||||
}
|
||||
|
||||
return { trackSettingsChange }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { canCreateVersionQuery } from '~/lib/graphql/mutationsAndQueries'
|
||||
import { canCreateModelIngestionQuery } from '~/lib/ingestion/graphql/queries'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
|
||||
// use this composable whenever we need to check against available graphqls over servers
|
||||
export function useCheckGraphql() {
|
||||
/**
|
||||
* Checks the ingestions available for the server,
|
||||
* if available, returns with respond by appending `queryAvailable = true`
|
||||
* otherwise, returns fake result object with `queryAvailable = false`
|
||||
*/
|
||||
const canCreateModelIngestion = async (
|
||||
projectId: string,
|
||||
modelId: string,
|
||||
accountId: string
|
||||
) => {
|
||||
const accountsStore = useAccountStore()
|
||||
const client = accountsStore.getAccountClient(accountId)
|
||||
try {
|
||||
const result = await client.query({
|
||||
query: canCreateModelIngestionQuery,
|
||||
variables: {
|
||||
projectId,
|
||||
modelId
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
return {
|
||||
...result.data.project.model.permissions.canCreateIngestion,
|
||||
queryAvailable: true
|
||||
}
|
||||
} catch {
|
||||
return { queryAvailable: false, authorized: false, message: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user can create a version for the given model.
|
||||
* Used to validate before starting a publish operation.
|
||||
*/
|
||||
const canCreateVersion = async (
|
||||
projectId: string,
|
||||
modelId: string,
|
||||
accountId: string
|
||||
) => {
|
||||
const accountsStore = useAccountStore()
|
||||
const client = accountsStore.getAccountClient(accountId)
|
||||
|
||||
try {
|
||||
const result = await client.query({
|
||||
query: canCreateVersionQuery,
|
||||
variables: {
|
||||
projectId,
|
||||
modelId
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
|
||||
return result.data.project.model.permissions.canCreateVersion
|
||||
} catch (error) {
|
||||
// If we can't check, allow the attempt - server will reject if not allowed
|
||||
console.error('Failed to check canCreateVersion:', error)
|
||||
return { authorized: true, message: null }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
canCreateVersion,
|
||||
canCreateModelIngestion
|
||||
}
|
||||
}
|
||||
@@ -86,12 +86,9 @@ export const workspaceListFragment = graphql(`
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
logo
|
||||
logoUrl
|
||||
role
|
||||
readOnly
|
||||
creationState {
|
||||
completed
|
||||
}
|
||||
permissions {
|
||||
canCreateProject {
|
||||
authorized
|
||||
@@ -249,6 +246,23 @@ export const canCreateModelInProjectQuery = graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
export const canCreateVersionQuery = graphql(`
|
||||
query CanCreateVersion($projectId: String!, $modelId: String!) {
|
||||
project(id: $projectId) {
|
||||
model(id: $modelId) {
|
||||
permissions {
|
||||
canCreateVersion {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const activeWorkspaceQuery = graphql(`
|
||||
query ActiveWorkspace {
|
||||
activeUser {
|
||||
@@ -459,10 +473,12 @@ export const projectDetailsQuery = graphql(`
|
||||
`)
|
||||
|
||||
export const automateFunctionsQuery = graphql(`
|
||||
query AutomateFunctions {
|
||||
automateFunctions {
|
||||
items {
|
||||
...AutomateFunctionItem
|
||||
query AutomateFunctions($workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
automateFunctions {
|
||||
items {
|
||||
...AutomateFunctionItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,7 +623,7 @@ export const projectUpdatedSubscription = graphql(`
|
||||
`)
|
||||
|
||||
export const modelViewingSubscription = graphql(`
|
||||
subscription Subscription($target: ViewerUpdateTrackingTarget!) {
|
||||
subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {
|
||||
viewerUserActivityBroadcasted(target: $target) {
|
||||
userName
|
||||
userId
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import {
|
||||
provideApolloClient,
|
||||
useMutation,
|
||||
useSubscription
|
||||
} from '@vue/apollo-composable'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
import {
|
||||
completeModelIngestionWithVersion,
|
||||
createModelIngestion,
|
||||
updateModelIngestionProgress,
|
||||
failModelIngestionWithError,
|
||||
failModelIngestionWithCancel
|
||||
} from '../graphql/mutations'
|
||||
import { projectModelIngestionUpdatedSubscription } from '../graphql/subscriptions'
|
||||
import type {
|
||||
SourceDataInput,
|
||||
ProjectModelIngestionUpdatedSubscription
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import type { ISenderModelCard } from '~/lib/models/card/send'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ToastNotificationType } from '@speckle/ui-components'
|
||||
|
||||
/**
|
||||
* New way of creating versions.
|
||||
* It is essential for server to track limits on versions.
|
||||
* The flow is as follows:
|
||||
* 0. Check if the user has enough limits to create a new version (this is handled outside of this composable)
|
||||
* 1. Start a new ingestion
|
||||
* 2. Update the ingestion with the new data when connector throws progress via 'setModelProgress' event
|
||||
* 3. Complete the version with the root object id that passed by connector or server/sketchup bridges in JS
|
||||
*/
|
||||
export const useModelIngestion = () => {
|
||||
const store = useHostAppStore()
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
|
||||
const startIngestion = async (
|
||||
senderModelCard: ISenderModelCard,
|
||||
progressMessage: string,
|
||||
sourceData: SourceDataInput
|
||||
) => {
|
||||
const { activeIngestions } = storeToRefs(store)
|
||||
const client = accountStore.getAccountClient(senderModelCard.accountId)
|
||||
const { mutate } = provideApolloClient(client)(() =>
|
||||
useMutation(createModelIngestion)
|
||||
)
|
||||
|
||||
const res = await mutate({
|
||||
input: {
|
||||
projectId: senderModelCard.projectId,
|
||||
modelId: senderModelCard.modelId,
|
||||
progressMessage,
|
||||
sourceData,
|
||||
maxIdleTimeoutSeconds: 7200 // 2h
|
||||
}
|
||||
})
|
||||
|
||||
if (res?.errors?.length) {
|
||||
const msg = res.errors[0].message
|
||||
store.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Ingestion Error',
|
||||
description: msg
|
||||
})
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
const ingestionId = res?.data?.projectMutations.modelIngestionMutations.create.id
|
||||
if (ingestionId) {
|
||||
activeIngestions.value[senderModelCard.modelCardId] = ingestionId
|
||||
}
|
||||
|
||||
return res?.data?.projectMutations.modelIngestionMutations.create
|
||||
}
|
||||
|
||||
const updateIngestion = async (
|
||||
senderModelCard: ISenderModelCard,
|
||||
ingestionId: string,
|
||||
progressMessage: string,
|
||||
progress?: number
|
||||
) => {
|
||||
const client = accountStore.getAccountClient(senderModelCard.accountId)
|
||||
const { mutate } = provideApolloClient(client)(() =>
|
||||
useMutation(updateModelIngestionProgress)
|
||||
)
|
||||
|
||||
const res = await mutate({
|
||||
input: {
|
||||
projectId: senderModelCard.projectId,
|
||||
ingestionId,
|
||||
progressMessage,
|
||||
progress
|
||||
}
|
||||
})
|
||||
|
||||
if (res?.errors?.length) {
|
||||
const msg = res.errors[0].message
|
||||
store.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Ingestion Error',
|
||||
description: msg
|
||||
})
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
return res?.data?.projectMutations.modelIngestionMutations.updateProgress
|
||||
}
|
||||
|
||||
const failIngestion = async (
|
||||
senderModelCard: ISenderModelCard,
|
||||
ingestionId: string,
|
||||
errorReason: string,
|
||||
errorStacktrace?: string
|
||||
) => {
|
||||
const client = accountStore.getAccountClient(senderModelCard.accountId)
|
||||
const { mutate } = provideApolloClient(client)(() =>
|
||||
useMutation(failModelIngestionWithError)
|
||||
)
|
||||
|
||||
const res = await mutate({
|
||||
input: {
|
||||
projectId: senderModelCard.projectId,
|
||||
ingestionId,
|
||||
errorReason,
|
||||
errorStacktrace
|
||||
}
|
||||
})
|
||||
|
||||
if (res?.errors?.length) {
|
||||
const msg = res.errors[0].message
|
||||
store.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Ingestion Error',
|
||||
description: msg
|
||||
})
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
const { activeIngestions } = storeToRefs(store)
|
||||
|
||||
// clean the failed ingestion
|
||||
activeIngestions.value = Object.fromEntries(
|
||||
Object.entries(activeIngestions.value).filter(
|
||||
([key]) => key !== senderModelCard.modelCardId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const cancelIngestion = async (
|
||||
senderModelCard: ISenderModelCard,
|
||||
ingestionId: string,
|
||||
cancellationMessage: string = 'Cancelled by user'
|
||||
) => {
|
||||
const client = accountStore.getAccountClient(senderModelCard.accountId)
|
||||
const { mutate } = provideApolloClient(client)(() =>
|
||||
useMutation(failModelIngestionWithCancel)
|
||||
)
|
||||
|
||||
const res = await mutate({
|
||||
input: {
|
||||
projectId: senderModelCard.projectId,
|
||||
ingestionId,
|
||||
cancellationMessage
|
||||
}
|
||||
})
|
||||
|
||||
if (res?.errors?.length) {
|
||||
const msg = res.errors[0].message
|
||||
store.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Ingestion Error',
|
||||
description: msg
|
||||
})
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
const { activeIngestions } = storeToRefs(store)
|
||||
|
||||
// clean the cancelled ingestion
|
||||
activeIngestions.value = Object.fromEntries(
|
||||
Object.entries(activeIngestions.value).filter(
|
||||
([key]) => key !== senderModelCard.modelCardId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const completeIngestionWithVersion = async (
|
||||
senderModelCard: ISenderModelCard,
|
||||
ingestionId: string,
|
||||
rootObjectId: string
|
||||
) => {
|
||||
const client = accountStore.getAccountClient(senderModelCard.accountId)
|
||||
const { mutate } = provideApolloClient(client)(() =>
|
||||
useMutation(completeModelIngestionWithVersion)
|
||||
)
|
||||
|
||||
const res = await mutate({
|
||||
input: {
|
||||
projectId: senderModelCard.projectId,
|
||||
ingestionId,
|
||||
rootObjectId
|
||||
}
|
||||
})
|
||||
|
||||
if (res?.errors?.length) {
|
||||
const msg = res.errors[0].message
|
||||
store.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Ingestion Error',
|
||||
description: msg
|
||||
})
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
const { activeIngestions } = storeToRefs(store)
|
||||
|
||||
// clean the completed ingestion
|
||||
activeIngestions.value = Object.fromEntries(
|
||||
Object.entries(activeIngestions.value).filter(
|
||||
([key]) => key !== senderModelCard.modelCardId
|
||||
)
|
||||
)
|
||||
|
||||
return res?.data?.projectMutations.modelIngestionMutations.completeWithVersion
|
||||
}
|
||||
|
||||
// Tracks active ingestion subscriptions so they can be stopped on cancel or terminal state
|
||||
const activeSubscriptions: Record<string, () => void> = {}
|
||||
|
||||
/**
|
||||
* Subscribes to ingestion status updates for a given ingestionId.
|
||||
* Used when the connector (.NET SDK) handles the ingestion and passes the ingestionId
|
||||
* back to the DUI via setModelSendResult. The DUI then subscribes to track
|
||||
* the server-side processing state until a terminal status is reached.
|
||||
*
|
||||
* Manages model card state directly: updates progress, sets versionId on success,
|
||||
* sets error on failure, and clears progress on terminal states.
|
||||
*/
|
||||
const subscribeToIngestion = (
|
||||
senderModelCard: ISenderModelCard,
|
||||
ingestionId: string
|
||||
) => {
|
||||
const client = accountStore.getAccountClient(senderModelCard.accountId)
|
||||
|
||||
senderModelCard.progress = { status: 'Remote processing...' }
|
||||
|
||||
const { onResult, onError, stop } = provideApolloClient(client)(() =>
|
||||
useSubscription(projectModelIngestionUpdatedSubscription, () => ({
|
||||
input: {
|
||||
projectId: senderModelCard.projectId,
|
||||
ingestionReference: { ingestionId }
|
||||
}
|
||||
}))
|
||||
)
|
||||
|
||||
activeSubscriptions[senderModelCard.modelCardId] = stop
|
||||
|
||||
onResult((result) => {
|
||||
const data = result.data as ProjectModelIngestionUpdatedSubscription | undefined
|
||||
const statusData = data?.projectModelIngestionUpdated?.modelIngestion?.statusData
|
||||
if (!statusData) return
|
||||
|
||||
switch (statusData.__typename) {
|
||||
case 'ModelIngestionSuccessStatus':
|
||||
senderModelCard.latestCreatedVersionId = statusData.versionId
|
||||
senderModelCard.progress = undefined
|
||||
unsubscribeFromIngestion(senderModelCard.modelCardId)
|
||||
break
|
||||
case 'ModelIngestionProcessingStatus':
|
||||
senderModelCard.progress = {
|
||||
status: statusData.progressMessage,
|
||||
progress: statusData.progress ?? undefined
|
||||
}
|
||||
break
|
||||
case 'ModelIngestionFailedStatus':
|
||||
senderModelCard.error = {
|
||||
errorMessage: statusData.errorReason,
|
||||
dismissible: true
|
||||
}
|
||||
senderModelCard.progress = undefined
|
||||
unsubscribeFromIngestion(senderModelCard.modelCardId)
|
||||
break
|
||||
case 'ModelIngestionCancelledStatus':
|
||||
senderModelCard.progress = undefined
|
||||
unsubscribeFromIngestion(senderModelCard.modelCardId)
|
||||
break
|
||||
case 'ModelIngestionQueuedStatus':
|
||||
senderModelCard.progress = {
|
||||
status: statusData.progressMessage
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
onError((err) => {
|
||||
console.error('Ingestion subscription error:', err)
|
||||
unsubscribeFromIngestion(senderModelCard.modelCardId)
|
||||
})
|
||||
}
|
||||
|
||||
const unsubscribeFromIngestion = (modelCardId: string) => {
|
||||
const stop = activeSubscriptions[modelCardId]
|
||||
if (stop) {
|
||||
stop()
|
||||
delete activeSubscriptions[modelCardId]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startIngestion,
|
||||
updateIngestion,
|
||||
failIngestion,
|
||||
cancelIngestion,
|
||||
completeIngestionWithVersion,
|
||||
subscribeToIngestion,
|
||||
unsubscribeFromIngestion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
export const createModelIngestion = graphql(`
|
||||
mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {
|
||||
projectMutations {
|
||||
modelIngestionMutations {
|
||||
create(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const updateModelIngestionProgress = graphql(`
|
||||
mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {
|
||||
projectMutations {
|
||||
modelIngestionMutations {
|
||||
updateProgress(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const completeModelIngestionWithVersion = graphql(`
|
||||
mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {
|
||||
projectMutations {
|
||||
modelIngestionMutations {
|
||||
completeWithVersion(input: $input) {
|
||||
id
|
||||
statusData {
|
||||
__typename
|
||||
... on ModelIngestionProcessingStatus {
|
||||
status
|
||||
progressMessage
|
||||
progress
|
||||
}
|
||||
... on ModelIngestionSuccessStatus {
|
||||
status
|
||||
versionId
|
||||
}
|
||||
... on ModelIngestionFailedStatus {
|
||||
errorStacktrace
|
||||
errorReason
|
||||
status
|
||||
}
|
||||
... on ModelIngestionCancelledStatus {
|
||||
cancellationMessage
|
||||
status
|
||||
}
|
||||
... on ModelIngestionQueuedStatus {
|
||||
progressMessage
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const failModelIngestionWithError = graphql(`
|
||||
mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {
|
||||
projectMutations {
|
||||
modelIngestionMutations {
|
||||
failWithError(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const failModelIngestionWithCancel = graphql(`
|
||||
mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {
|
||||
projectMutations {
|
||||
modelIngestionMutations {
|
||||
failWithCancel(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -0,0 +1,17 @@
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
export const canCreateModelIngestionQuery = graphql(`
|
||||
query CanCreateIngestion($modelId: String!, $projectId: String!) {
|
||||
project(id: $projectId) {
|
||||
model(id: $modelId) {
|
||||
permissions {
|
||||
canCreateIngestion {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -0,0 +1,38 @@
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
export const projectModelIngestionUpdatedSubscription = graphql(`
|
||||
subscription ProjectModelIngestionUpdated(
|
||||
$input: ProjectModelIngestionSubscriptionInput!
|
||||
) {
|
||||
projectModelIngestionUpdated(input: $input) {
|
||||
type
|
||||
modelIngestion {
|
||||
id
|
||||
statusData {
|
||||
__typename
|
||||
... on ModelIngestionSuccessStatus {
|
||||
status
|
||||
versionId
|
||||
}
|
||||
... on ModelIngestionProcessingStatus {
|
||||
status
|
||||
progressMessage
|
||||
progress
|
||||
}
|
||||
... on ModelIngestionFailedStatus {
|
||||
status
|
||||
errorReason
|
||||
}
|
||||
... on ModelIngestionCancelledStatus {
|
||||
status
|
||||
cancellationMessage
|
||||
}
|
||||
... on ModelIngestionQueuedStatus {
|
||||
status
|
||||
progressMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -0,0 +1,71 @@
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
export const issueFragment = graphql(`
|
||||
fragment IssuesItem on Issue {
|
||||
id
|
||||
status
|
||||
title
|
||||
priority
|
||||
viewerState
|
||||
identifier
|
||||
resourceIdString
|
||||
activities(input: { limit: 1, sortDirection: asc }) {
|
||||
totalCount
|
||||
items {
|
||||
actor {
|
||||
id
|
||||
user {
|
||||
name
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
eventType
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
replies {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
author {
|
||||
id
|
||||
user {
|
||||
name
|
||||
id
|
||||
avatar
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
description {
|
||||
doc
|
||||
}
|
||||
}
|
||||
}
|
||||
description {
|
||||
doc
|
||||
}
|
||||
labels {
|
||||
hexColor
|
||||
id
|
||||
name
|
||||
}
|
||||
author {
|
||||
id
|
||||
user {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
}
|
||||
}
|
||||
dueDate
|
||||
assignee {
|
||||
id
|
||||
user {
|
||||
id
|
||||
avatar
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -0,0 +1,35 @@
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
export const issuesListQuery = graphql(`
|
||||
query IssuesList($projectId: String!) {
|
||||
project(id: $projectId) {
|
||||
id
|
||||
issues {
|
||||
totalCount
|
||||
items {
|
||||
...IssuesItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const issueResourceMetaSearchQuery = graphql(`
|
||||
query IssueResourceMetaSearch(
|
||||
$workspaceId: String!
|
||||
$resourceType: ResourceMetaType!
|
||||
$resourceId: String!
|
||||
$projectId: String
|
||||
$metaType: String
|
||||
) {
|
||||
resourceMetaSearch(
|
||||
workspaceId: $workspaceId
|
||||
resourceType: $resourceType
|
||||
resourceId: $resourceId
|
||||
projectId: $projectId
|
||||
metaType: $metaType
|
||||
) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -0,0 +1,7 @@
|
||||
export type Label = {
|
||||
id: string
|
||||
name: string
|
||||
hexColor?: string
|
||||
}
|
||||
|
||||
export type LabelsValue = Label[]
|
||||
@@ -15,6 +15,7 @@ export type ModelCardNotification = {
|
||||
name: string
|
||||
tooltipText?: string
|
||||
action: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
/**
|
||||
* If set, will display a view report button next to cta
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { computed } from 'vue'
|
||||
import type {
|
||||
ISendFilter,
|
||||
SendFilterSelect,
|
||||
RevitCategoriesSendFilter,
|
||||
RevitViewsSendFilter
|
||||
} from '~/lib/models/card/send'
|
||||
import { ValidationHelpers } from '@speckle/ui-components'
|
||||
import type { GenericValidateFunction } from 'vee-validate'
|
||||
|
||||
export const isSelectFilter = (f: ISendFilter): f is SendFilterSelect =>
|
||||
f.type === 'Select' || 'selectedItems' in f
|
||||
|
||||
export const isRevitCategoriesFilter = (
|
||||
f: ISendFilter
|
||||
): f is RevitCategoriesSendFilter =>
|
||||
f.id === 'revitCategories' || f.id === 'archicadLayers'
|
||||
|
||||
export const isRevitViewsFilter = (f: ISendFilter): f is RevitViewsSendFilter =>
|
||||
f.id === 'revitViews'
|
||||
|
||||
export const isEmail = ValidationHelpers.isEmail
|
||||
|
||||
export const isOneOrMultipleEmails = ValidationHelpers.isOneOrMultipleEmails
|
||||
@@ -42,3 +60,42 @@ export function useModelNameValidationRules() {
|
||||
isValidModelName
|
||||
])
|
||||
}
|
||||
|
||||
export type FilterValidationResult = { valid: boolean; reason?: string }
|
||||
|
||||
export function validateFilter(
|
||||
filter: ISendFilter | undefined,
|
||||
context: { selectionCount: number }
|
||||
): FilterValidationResult {
|
||||
if (!filter) return { valid: false, reason: 'No filter selected' }
|
||||
|
||||
// Selection Filter check
|
||||
if (filter.name === 'Selection' || filter.id === 'selection') {
|
||||
return context.selectionCount > 0
|
||||
? { valid: true }
|
||||
: { valid: false, reason: 'No objects selected to publish' }
|
||||
}
|
||||
|
||||
// List-based filters (Rhino Layers, etc.)
|
||||
if (isSelectFilter(filter)) {
|
||||
return (filter.selectedItems?.length ?? 0) > 0
|
||||
? { valid: true }
|
||||
: { valid: false, reason: 'No items selected to publish' }
|
||||
}
|
||||
|
||||
// Category-based filters
|
||||
if (isRevitCategoriesFilter(filter)) {
|
||||
return (filter.selectedCategories?.length ?? 0) > 0
|
||||
? { valid: true }
|
||||
: { valid: false, reason: 'No categories selected to publish' }
|
||||
}
|
||||
|
||||
// View-based filters
|
||||
if (isRevitViewsFilter(filter)) {
|
||||
return filter.selectedView?.trim()
|
||||
? { valid: true }
|
||||
: { valid: false, reason: 'No view selected to publish' }
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
export const workspacePlanUsageUpdatedSubscription = graphql(`
|
||||
subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {
|
||||
workspacePlanUsageUpdated(input: $input)
|
||||
}
|
||||
`)
|
||||
+26
-1
@@ -10,6 +10,13 @@
|
||||
"build": "nuxt build",
|
||||
"dev:nuxt": "nuxt dev",
|
||||
"dev": "concurrently \"nuxt dev\" \"yarn gqlgen:watch\"",
|
||||
"dev:kind:up": "ctlptl apply --filename ./tests/deployment/helm/cluster-config.yaml",
|
||||
"dev:kind:down": "ctlptl delete -f ./tests/deployment/helm/cluster-config.yaml",
|
||||
"dev:kind:helm:up": "yarn run dev:kind:up && tilt up --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui",
|
||||
"dev:kind:helm:down": "tilt down --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui",
|
||||
"dev:kind:helm:ci": "tilt ci --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui --timeout 10m",
|
||||
"docker:build": "docker build -f ./deployment/docker/Dockerfile -t ghcr.io/specklesystems/speckle-dui:local .",
|
||||
"docker:run": "docker run --rm -p 8083:80 -v ./deployment/docker/configuration.toml:/app/configuration.toml:ro ghcr.io/specklesystems/speckle-dui:local",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
@@ -20,7 +27,8 @@
|
||||
"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"
|
||||
"gqlgen:watch": "graphql-codegen --watch",
|
||||
"prettier:fix": "prettier --config .prettierrc --ignore-path .prettierignore --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.14",
|
||||
@@ -38,6 +46,22 @@
|
||||
"@speckle/tailwind-theme": "2.25.0",
|
||||
"@speckle/ui-components": "^2.25.0",
|
||||
"@speckle/ui-components-nuxt": "^2.25.0",
|
||||
"@tiptap/core": "2.10.3",
|
||||
"@tiptap/extension-bold": "2.10.3",
|
||||
"@tiptap/extension-document": "2.10.3",
|
||||
"@tiptap/extension-hard-break": "2.10.3",
|
||||
"@tiptap/extension-history": "2.10.3",
|
||||
"@tiptap/extension-italic": "2.10.3",
|
||||
"@tiptap/extension-link": "2.10.3",
|
||||
"@tiptap/extension-mention": "2.10.3",
|
||||
"@tiptap/extension-paragraph": "2.10.3",
|
||||
"@tiptap/extension-placeholder": "2.10.3",
|
||||
"@tiptap/extension-strike": "2.10.3",
|
||||
"@tiptap/extension-text": "2.10.3",
|
||||
"@tiptap/extension-underline": "2.10.3",
|
||||
"@tiptap/pm": "2.10.3",
|
||||
"@tiptap/suggestion": "2.10.3",
|
||||
"@tiptap/vue-3": "2.10.3",
|
||||
"@vue/apollo-composable": "^4.0.0-beta.5",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
@@ -47,6 +71,7 @@
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-vue-next": "^0.537.0",
|
||||
"nanoevents": "^8.0.0",
|
||||
"pinia": "^2.1.4",
|
||||
"portal-vue": "^3.0.0",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center"><InfiniteLoading /></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ToastNotificationType } from '@speckle/ui-components'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthManager } from '~/lib/authn/useAuthManager'
|
||||
import { useTokenExchange } from '~/lib/authn/useTokenExchange'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { getChallenge, getCodeVerifier, getChallengeUrl } = useAuthManager()
|
||||
const { exchangeAccessCode } = useTokenExchange()
|
||||
const hostApp = useHostAppStore()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const origin = getChallengeUrl()
|
||||
const accessCode = route.query.access_code as string | undefined
|
||||
if (accessCode && origin) {
|
||||
const challenge = getChallenge()
|
||||
if (!challenge) {
|
||||
throw new Error('No challenge found in storage.')
|
||||
}
|
||||
const codeVerifier = getCodeVerifier() ?? undefined
|
||||
await exchangeAccessCode(origin, accessCode, challenge, codeVerifier)
|
||||
} else {
|
||||
throw new Error('No access code is found.')
|
||||
}
|
||||
} catch (error) {
|
||||
hostApp.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Failed to add your Speckle account.',
|
||||
description: error instanceof Error ? error.message : (error as string)
|
||||
})
|
||||
} finally {
|
||||
router.replace('/')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<!-- Step 1: Mapping Mode Selection -->
|
||||
<div class="px-2">
|
||||
<p class="h5">Mapping Mode</p>
|
||||
<p class="h5">Assign by</p>
|
||||
<div class="space-y-2 my-2">
|
||||
<FormSelectBase
|
||||
:model-value="selectedMappingMode"
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IAccountBindingKey,
|
||||
MockedAccountBinding
|
||||
} from '~/lib/bindings/definitions/IAccountBinding'
|
||||
import type { IParametersBinding } from '~/lib/bindings/definitions/IParametersBinding'
|
||||
|
||||
import type { ITestBinding } from '~/lib/bindings/definitions/ITestBinding'
|
||||
import {
|
||||
@@ -132,6 +133,10 @@ export default defineNuxtPlugin(async () => {
|
||||
ITopLevelExpectionHandlerBindingKey
|
||||
)
|
||||
|
||||
const parametersBinding = await tryHoistBinding<IParametersBinding>(
|
||||
'parametersBinding'
|
||||
)
|
||||
|
||||
// Any binding implments these two methods below, we just choose one to
|
||||
// expose globally to the app.
|
||||
const showDevTools = () => {
|
||||
@@ -157,7 +162,8 @@ export default defineNuxtPlugin(async () => {
|
||||
topLevelExceptionHandlerBinding,
|
||||
showDevTools,
|
||||
openUrl,
|
||||
revitMapperBinding
|
||||
revitMapperBinding,
|
||||
parametersBinding
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import Intercom, {
|
||||
trackEvent
|
||||
} from '@intercom/messenger-js-sdk'
|
||||
import { useAccountStore } from '~/store/accounts'
|
||||
import { useHostAppStore } from '~/store/hostApp'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const disabledRoutes: string[] = []
|
||||
@@ -15,7 +16,9 @@ export const useIntercom = () => {
|
||||
const route = useRoute()
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const hostAppStore = useHostAppStore()
|
||||
const { activeAccount } = storeToRefs(accountStore)
|
||||
const { isDistributedBySpeckle } = storeToRefs(hostAppStore)
|
||||
|
||||
const isInitialized = ref(false)
|
||||
|
||||
@@ -80,6 +83,13 @@ export const useIntercom = () => {
|
||||
}
|
||||
})
|
||||
|
||||
// we listen to changes in the host app distribution status that fetched on updateConnector composable after the intercom is initialized, we cant simply rely on activeAccount watcher
|
||||
watch(isDistributedBySpeckle, (newValue) => {
|
||||
if (!newValue) {
|
||||
shutdownIntercom()
|
||||
}
|
||||
})
|
||||
|
||||
watch(activeAccount, (newValue) => {
|
||||
if (newValue) {
|
||||
if (!isInitialized.value) {
|
||||
|
||||
+19
-8
@@ -165,14 +165,13 @@ export const useAccountStore = defineStore('accountStore', () => {
|
||||
// hostAppStore.setNotification(notification)
|
||||
}
|
||||
|
||||
// if (res.networkError) {
|
||||
// const notification: ToastNotification = {
|
||||
// type: ToastNotificationType.Danger,
|
||||
// title: 'Network Error',
|
||||
// description: res.networkError.message
|
||||
// }
|
||||
// hostAppStore.setNotification(notification)
|
||||
// }
|
||||
if (res.networkError && !navigator.onLine) {
|
||||
hostAppStore.setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'No Internet Connection',
|
||||
description: 'Please check your network connection and try again.'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const link = splitLink(
|
||||
@@ -286,12 +285,23 @@ export const useAccountStore = defineStore('accountStore', () => {
|
||||
}
|
||||
|
||||
const accountByServerUrl = (serverUrl: string) => {
|
||||
if (activeAccount.value.accountInfo.serverInfo.url === serverUrl) {
|
||||
return activeAccount.value
|
||||
}
|
||||
const accountMatchWithServerUrl = accounts.value.find(
|
||||
(acc) => acc.accountInfo.serverInfo.url === serverUrl
|
||||
)
|
||||
if (accountMatchWithServerUrl) return accountMatchWithServerUrl
|
||||
}
|
||||
|
||||
const getAccountClient = (accountId: string) => {
|
||||
return (
|
||||
accounts.value.find(
|
||||
(account) => account.accountInfo.id === accountId
|
||||
) as DUIAccount
|
||||
).client
|
||||
}
|
||||
|
||||
const provideClients = () => {
|
||||
provideApolloClients(apolloClients)
|
||||
}
|
||||
@@ -318,6 +328,7 @@ export const useAccountStore = defineStore('accountStore', () => {
|
||||
return {
|
||||
isLoading,
|
||||
accounts,
|
||||
getAccountClient,
|
||||
defaultAccount,
|
||||
activeAccount,
|
||||
userSelectedAccount,
|
||||
|
||||
+13
-2
@@ -8,11 +8,14 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
|
||||
const userSelectedWorkspaceId = ref<string>()
|
||||
|
||||
const config = ref<ConnectorConfig>({ darkTheme: true })
|
||||
const config = ref<ConnectorConfig>({ darkTheme: true, disableCache: false })
|
||||
|
||||
const isDarkTheme = computed(() => {
|
||||
return config.value?.darkTheme
|
||||
})
|
||||
const isCacheDisabled = computed(() => {
|
||||
return config.value?.disableCache || false
|
||||
})
|
||||
const isDevMode = ref(false)
|
||||
|
||||
const toggleTheme = () => {
|
||||
@@ -20,6 +23,11 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
$configBinding.updateConfig(config.value)
|
||||
}
|
||||
|
||||
const toggleCache = () => {
|
||||
config.value.disableCache = !config.value.disableCache
|
||||
$configBinding.updateConfig(config.value)
|
||||
}
|
||||
|
||||
const setUserSelectedWorkspace = (workspaceId: string) => {
|
||||
userSelectedWorkspaceId.value = workspaceId
|
||||
try {
|
||||
@@ -33,7 +41,8 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
|
||||
const init = async () => {
|
||||
if (!$configBinding) return
|
||||
config.value = await $configBinding.getConfig()
|
||||
const fetchedConfig = await $configBinding.getConfig()
|
||||
config.value = { disableCache: false, ...fetchedConfig }
|
||||
const workspacesConfig = await $configBinding.getWorkspacesConfig()
|
||||
if (workspacesConfig && workspacesConfig.userSelectedWorkspaceId) {
|
||||
userSelectedWorkspaceId.value = workspacesConfig.userSelectedWorkspaceId
|
||||
@@ -51,9 +60,11 @@ export const useConfigStore = defineStore('configStore', () => {
|
||||
config,
|
||||
hasConfigBindings,
|
||||
isDarkTheme,
|
||||
isCacheDisabled,
|
||||
isDevMode,
|
||||
userSelectedWorkspaceId,
|
||||
toggleTheme,
|
||||
toggleCache,
|
||||
setUserSelectedWorkspace
|
||||
}
|
||||
})
|
||||
|
||||
+191
-28
@@ -13,7 +13,10 @@ import type {
|
||||
RevitViewsSendFilter,
|
||||
SendFilterSelect
|
||||
} from '~/lib/models/card/send'
|
||||
import { useSelectionStore } from '~/store/selection'
|
||||
import { validateFilter } from '~/lib/validation'
|
||||
import type { ToastNotification } from '@speckle/ui-components'
|
||||
import { ToastNotificationType } from '@speckle/ui-components'
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
import type { HostAppError } from '~/lib/bridge/errorHandler'
|
||||
import type { ConversionResult } from '~/lib/conversions/conversionResult'
|
||||
@@ -28,6 +31,8 @@ import {
|
||||
import { provideApolloClient, useMutation } from '@vue/apollo-composable'
|
||||
import { createVersionMutation } from '~/lib/graphql/mutationsAndQueries'
|
||||
import type { BaseBridge } from '~/lib/bridge/base'
|
||||
import { useModelIngestion } from '~/lib/ingestion/composables/useModelIngestion'
|
||||
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
|
||||
|
||||
export type ProjectModelGroup = {
|
||||
projectId: string
|
||||
@@ -43,7 +48,15 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
const { $openUrl } = useNuxtApp()
|
||||
const accountsStore = useAccountStore()
|
||||
const { checkUpdate } = useUpdateConnector()
|
||||
|
||||
const {
|
||||
startIngestion,
|
||||
updateIngestion,
|
||||
failIngestion,
|
||||
cancelIngestion,
|
||||
completeIngestionWithVersion,
|
||||
subscribeToIngestion,
|
||||
unsubscribeFromIngestion
|
||||
} = useModelIngestion()
|
||||
const isDistributedBySpeckle = ref<boolean>(true)
|
||||
const latestAvailableVersion = ref<Version | null>(null)
|
||||
|
||||
@@ -65,6 +78,9 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
// Different host apps can have different kind of ISendFilterSelect send filters, and we collect them here to generalize the component we use in `ListSelect`
|
||||
const availableSelectSendFilters = ref<Record<string, SendFilterSelect>>({})
|
||||
|
||||
// kvp for modelCardId - ingestionId
|
||||
const activeIngestions = ref<Record<string, string>>({})
|
||||
|
||||
const dismissNotification = () => {
|
||||
currentNotification.value = null
|
||||
}
|
||||
@@ -81,9 +97,11 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
$openUrl(latestAvailableVersion.value?.Url as string)
|
||||
}
|
||||
|
||||
const isConnectorUpToDate = computed(
|
||||
() => connectorVersion.value === latestAvailableVersion.value?.Number
|
||||
)
|
||||
const isConnectorUpToDate = computed(() => {
|
||||
if (!isDistributedBySpeckle.value) return true
|
||||
if (!latestAvailableVersion.value?.Number || !connectorVersion.value) return true
|
||||
return connectorVersion.value === latestAvailableVersion.value.Number
|
||||
})
|
||||
|
||||
const setHostAppError = (error: Nullable<HostAppError>) => {
|
||||
hostAppError.value = error
|
||||
@@ -93,6 +111,11 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
isDistributedBySpeckle.value = val
|
||||
}
|
||||
|
||||
const shouldHandleIngestion = computed(() => {
|
||||
const hostAppsThatUsesDUIForGraphql = ['sketchup', 'archicad', 'Vectorworks']
|
||||
return hostAppsThatUsesDUIForGraphql.includes(hostAppName.value as string)
|
||||
})
|
||||
|
||||
/**
|
||||
* Model Card Operations
|
||||
*/
|
||||
@@ -279,20 +302,59 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
const account = accountStore.accounts.find(
|
||||
(acc) => acc.accountInfo.id === args.accountId
|
||||
)
|
||||
try {
|
||||
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
|
||||
useMutation(createVersionMutation)
|
||||
)
|
||||
await createVersion.mutate({
|
||||
input: {
|
||||
modelId: args.modelId,
|
||||
objectId: args.referencedObjectId,
|
||||
sourceApplication: args.sourceApplication,
|
||||
projectId: args.projectId
|
||||
|
||||
// Check if we have an ingestion ID for this model.
|
||||
// If so, we are in the "New Business Model" flow and should use completeIngestionWithVersion.
|
||||
const modelCard = documentModelStore.value.models.find(
|
||||
(m) => m.modelId === args.modelId && m.projectId === args.projectId
|
||||
) as ISenderModelCard
|
||||
|
||||
const { canCreateModelIngestion } = useCheckGraphql()
|
||||
const canCreateIngestion = await canCreateModelIngestion(
|
||||
args.projectId,
|
||||
args.modelId,
|
||||
args.accountId
|
||||
)
|
||||
|
||||
if (canCreateIngestion.queryAvailable) {
|
||||
const ingestionId = modelCard
|
||||
? activeIngestions.value[modelCard.modelCardId]
|
||||
: undefined
|
||||
|
||||
if (ingestionId && modelCard) {
|
||||
try {
|
||||
await completeIngestionWithVersion(
|
||||
modelCard,
|
||||
ingestionId,
|
||||
args.referencedObjectId
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(`completeIngestionWithVersion failed: ${err}`)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`triggerCreateVersion is failed: ${err}`)
|
||||
} else {
|
||||
setNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Publish Error',
|
||||
description: 'Could not complete publish: Ingestion ID missing.'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Fallback to legacy flow (Old Server)
|
||||
try {
|
||||
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
|
||||
useMutation(createVersionMutation)
|
||||
)
|
||||
await createVersion.mutate({
|
||||
input: {
|
||||
modelId: args.modelId,
|
||||
objectId: args.referencedObjectId,
|
||||
sourceApplication: args.sourceApplication,
|
||||
projectId: args.projectId
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`triggerCreateVersion is failed: ${err}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -308,6 +370,14 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
*/
|
||||
app.$sendBinding?.on('refreshSendFilters', () => void refreshSendFilters())
|
||||
|
||||
const validateSendFilter = (filter?: ISendFilter) => {
|
||||
const selectionStore = useSelectionStore()
|
||||
|
||||
return validateFilter(filter, {
|
||||
selectionCount: selectionStore.selectionInfo.selectedObjectIds?.length ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send functionality
|
||||
*/
|
||||
@@ -316,10 +386,57 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
* Tells the host app to start sending a specific model card. This will reach inside the host application.
|
||||
* @param modelId
|
||||
*/
|
||||
const sendModel = (modelCardId: string, actionSource: string) => {
|
||||
const sendModel = async (modelCardId: string, actionSource: string) => {
|
||||
const model = documentModelStore.value.models.find(
|
||||
(m) => m.modelCardId === modelCardId
|
||||
) as ISenderModelCard
|
||||
const { canCreateModelIngestion, canCreateVersion } = useCheckGraphql()
|
||||
const canCreateIngestion = await canCreateModelIngestion(
|
||||
model.projectId,
|
||||
model.modelId,
|
||||
model.accountId
|
||||
)
|
||||
|
||||
// for the connectors that don't have SDK to handle graqhql
|
||||
if (shouldHandleIngestion.value && canCreateIngestion.queryAvailable) {
|
||||
const sourceData = {
|
||||
sourceApplicationSlug: hostAppName.value || 'unknown',
|
||||
sourceApplicationVersion: hostAppVersion.value?.toString() || 'unknown'
|
||||
}
|
||||
if (canCreateIngestion.authorized) {
|
||||
await startIngestion(model, 'Starting to publish', sourceData)
|
||||
model.progress = { status: 'Converting the objects...' }
|
||||
} else {
|
||||
setNotification({
|
||||
type: ToastNotificationType.Warning,
|
||||
title: 'Cannot publish',
|
||||
description: canCreateIngestion.message
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// for the self hosters that does not have available graphql for ingestions
|
||||
const canCreate = await canCreateVersion(
|
||||
model.projectId,
|
||||
model.modelId,
|
||||
model.accountId
|
||||
)
|
||||
if (!canCreate.authorized) {
|
||||
setNotification({
|
||||
type: ToastNotificationType.Warning,
|
||||
title: 'Cannot publish',
|
||||
description: canCreate.message || 'Workspace limits have been reached'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
model.latestCreatedVersionId = undefined
|
||||
model.error = undefined
|
||||
model.progress = { status: 'Starting to send...' }
|
||||
model.expired = false
|
||||
model.report = undefined
|
||||
|
||||
if (model.expired) {
|
||||
// user sends via "Update" button
|
||||
void trackEvent(
|
||||
@@ -344,11 +461,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
model.accountId
|
||||
)
|
||||
}
|
||||
model.latestCreatedVersionId = undefined
|
||||
model.error = undefined
|
||||
model.progress = { status: 'Starting to send...' }
|
||||
model.expired = false
|
||||
model.report = undefined
|
||||
|
||||
// You should stop asking why if you saw anything related autocad..
|
||||
// It solves the press "escape" issue.
|
||||
// Because probably we don't give enough time to acad complete it's previos task and it stucks.
|
||||
@@ -375,6 +488,17 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
model.error = undefined
|
||||
void trackEvent('DUI3 Action', { name: 'Send Cancel' }, model.accountId)
|
||||
model.latestCreatedVersionId = undefined
|
||||
|
||||
// Clean up any active ingestion subscription from SDK-based connectors
|
||||
unsubscribeFromIngestion(modelCardId)
|
||||
|
||||
// Cancel the ingestion if applicable
|
||||
if (shouldHandleIngestion.value) {
|
||||
const ingestionId = activeIngestions.value[modelCardId]
|
||||
if (ingestionId) {
|
||||
await cancelIngestion(model, ingestionId, 'Cancelled by user')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.$sendBinding?.on('setModelsExpired', (modelCardIds) => {
|
||||
@@ -391,13 +515,22 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
modelCardId: string
|
||||
versionId: string
|
||||
sendConversionResults: ConversionResult[]
|
||||
ingestionId?: string
|
||||
}) => {
|
||||
const model = documentModelStore.value.models.find(
|
||||
(m) => m.modelCardId === args.modelCardId
|
||||
) as ISenderModelCard
|
||||
model.latestCreatedVersionId = args.versionId
|
||||
// Conversion results are always valid regardless of ingestion state
|
||||
model.report = args.sendConversionResults
|
||||
model.progress = undefined
|
||||
|
||||
if (args.ingestionId) {
|
||||
// Connector handled ingestion via SDK — composable subscribes and manages model card state to 'Version created' bla bla
|
||||
subscribeToIngestion(model, args.ingestionId)
|
||||
} else {
|
||||
// Legacy path or no ingestion — behave as before
|
||||
model.latestCreatedVersionId = args.versionId
|
||||
model.progress = undefined
|
||||
}
|
||||
}
|
||||
|
||||
app.$sendBinding?.on('setModelSendResult', setModelSendResult)
|
||||
@@ -477,7 +610,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
app.$receiveBinding?.on('setModelReceiveResult', setModelReceiveResult)
|
||||
|
||||
// GENERIC STUFF
|
||||
const handleModelProgressEvents = (args: {
|
||||
const handleModelProgressEvents = async (args: {
|
||||
modelCardId: string
|
||||
progress?: ModelCardProgress
|
||||
}) => {
|
||||
@@ -485,9 +618,24 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
(m) => m.modelCardId === args.modelCardId
|
||||
) as IModelCard
|
||||
model.progress = args.progress
|
||||
|
||||
if (
|
||||
model.typeDiscriminator.includes('SenderModelCard') &&
|
||||
shouldHandleIngestion.value // for the connectors that don't have SDK to handle graqhql
|
||||
) {
|
||||
const ingestionId = activeIngestions.value[args.modelCardId]
|
||||
if (ingestionId) {
|
||||
await updateIngestion(
|
||||
model,
|
||||
ingestionId,
|
||||
args.progress?.status || 'Progressing',
|
||||
args.progress?.progress || 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setModelError = (args: {
|
||||
const setModelError = async (args: {
|
||||
modelCardId: string
|
||||
error: string | { errorMessage: string; dismissible?: boolean }
|
||||
}) => {
|
||||
@@ -503,6 +651,19 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
dismissible: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Fail the ingestion if applicable
|
||||
if (
|
||||
model.typeDiscriminator.includes('SenderModelCard') &&
|
||||
shouldHandleIngestion.value
|
||||
) {
|
||||
const ingestionId = activeIngestions.value[args.modelCardId]
|
||||
if (ingestionId) {
|
||||
const errorMessage =
|
||||
typeof args.error === 'string' ? args.error : args.error.errorMessage
|
||||
await failIngestion(model as ISenderModelCard, ingestionId, errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: all bindings that need to send these model events should register.
|
||||
@@ -748,6 +909,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
hostAppName,
|
||||
hostAppVersion,
|
||||
connectorVersion,
|
||||
activeIngestions,
|
||||
isConnectorUpToDate,
|
||||
latestAvailableVersion,
|
||||
documentInfo,
|
||||
@@ -786,6 +948,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
||||
getSendSettings,
|
||||
setModelSendResult,
|
||||
setModelReceiveResult,
|
||||
handleModelProgressEvents
|
||||
handleModelProgressEvents,
|
||||
validateSendFilter
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
print('🚀 Deploying Speckle DUI into a Kind Cluster via Tilt...')
|
||||
|
||||
# we limit tilt to run only on the kind cluster
|
||||
allow_k8s_contexts(['kind-speckle-dui'])
|
||||
if k8s_context() != 'kind-speckle-dui':
|
||||
fail('Failing early as tilt should only ever connect to kind-speckle-dui.')
|
||||
|
||||
# Install extensions
|
||||
load('ext://helm_resource', 'helm_resource', 'helm_repo')
|
||||
load('ext://k8s_yaml_glob', 'k8s_yaml_glob')
|
||||
|
||||
docker_build('ghcr.io/specklesystems/speckle-dui',
|
||||
context='../../..',
|
||||
dockerfile='../../../deployment/docker/Dockerfile',
|
||||
ignore = ['**/.nuxt', '**/node_modules', '**/dist', '**/build', '**/.git', '**/.claude', '**/.cursor', '**/deployment/**/*', '**/tests/**/*']
|
||||
)
|
||||
|
||||
# Create namespaces
|
||||
k8s_yaml_glob('./manifests/*.namespace.yaml')
|
||||
|
||||
k8s_yaml('./manifests/coredns.configmap.yaml')
|
||||
k8s_resource(new_name='coredns',
|
||||
objects=['coredns:configmap:kube-system'],
|
||||
resource_deps=[],
|
||||
labels=['coredns'])
|
||||
|
||||
# Update CoreDNS to allow for local resolution of services internally (i.e. speckle.internal will be routed to nginx)
|
||||
local_resource('coredns-up',
|
||||
cmd='./scripts/coredns-up.sh',
|
||||
resource_deps=['coredns'],
|
||||
deps=['./manifests/coredns.configmap.yaml', './scripts/coredns-up.sh'],
|
||||
labels=['coredns'])
|
||||
|
||||
helm_repo('ingress-nginx-repo',
|
||||
'https://kubernetes.github.io/ingress-nginx')
|
||||
|
||||
#nginx should be deployed as the last dependency as it opens ports to services
|
||||
#it expects these services to exist, which are created by the helm charts above
|
||||
helm_resource('ingress-nginx',
|
||||
release_name='ingress-nginx',
|
||||
namespace='ingress-nginx',
|
||||
chart='ingress-nginx-repo/ingress-nginx',
|
||||
flags=['--version=4.8.0',
|
||||
'--values=./values/nginx.values.yaml',
|
||||
'--kube-context=kind-speckle-dui'],
|
||||
deps=['./values/nginx.values.yaml'],
|
||||
resource_deps=['ingress-nginx-repo', 'coredns'],
|
||||
labels=['speckle-dependencies'])
|
||||
|
||||
helm_resource('speckle-dui',
|
||||
release_name='speckle-dui',
|
||||
namespace='speckle-dui',
|
||||
chart='./../../../deployment/helm/speckle-dui',
|
||||
flags=['--values=./values/speckle-dui.values.yaml',
|
||||
'--kube-context=kind-speckle-dui'],
|
||||
image_deps=[
|
||||
'ghcr.io/specklesystems/speckle-dui'
|
||||
],
|
||||
image_keys=[
|
||||
('image.repository', 'image.tag')
|
||||
],
|
||||
deps=['./../../../deployment/helm/speckle-dui',
|
||||
'./values/speckle-dui.values.yaml'],
|
||||
resource_deps=['ingress-nginx', 'coredns'],
|
||||
labels=['speckle-dui'])
|
||||
@@ -0,0 +1,29 @@
|
||||
apiVersion: ctlptl.dev/v1alpha1
|
||||
kind: Registry
|
||||
name: ctlptl-registry
|
||||
port: 5000
|
||||
---
|
||||
apiVersion: ctlptl.dev/v1alpha1
|
||||
kind: Cluster
|
||||
product: kind
|
||||
registry: ctlptl-registry
|
||||
name: kind-speckle-dui
|
||||
kindV1Alpha4Cluster:
|
||||
nodes:
|
||||
- role: control-plane
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: InitConfiguration
|
||||
nodeRegistration:
|
||||
kubeletExtraArgs:
|
||||
node-labels: "ingress-ready=true"
|
||||
extraMounts: []
|
||||
extraPortMappings:
|
||||
- containerPort: 80
|
||||
hostPort: 80 # Docker requires privileged ports binding permissions https://docs.docker.com/desktop/mac/permission-requirements/#binding-privileged-ports
|
||||
protocol: TCP
|
||||
listenAddress: '127.0.0.1' #DO NOT REMOVE - this is required to prevent access from the local network or the world!!!
|
||||
- containerPort: 443
|
||||
hostPort: 443 # Docker requires privileged ports binding permissions https://docs.docker.com/desktop/mac/permission-requirements/#binding-privileged-ports
|
||||
protocol: TCP
|
||||
listenAddress: '127.0.0.1' #DO NOT REMOVE - this is required to prevent access from the local network or the world!!!
|
||||
@@ -0,0 +1,24 @@
|
||||
{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz") {} }:
|
||||
|
||||
let
|
||||
corepack = pkgs.stdenv.mkDerivation {
|
||||
name = "corepack";
|
||||
buildInputs = [ pkgs.nodejs_22 ];
|
||||
phases = [ "installPhase" ];
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
corepack enable --install-directory=$out/bin
|
||||
'';
|
||||
};
|
||||
in pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.docker
|
||||
pkgs.kind
|
||||
pkgs.kubectl
|
||||
pkgs.nodejs_22
|
||||
pkgs.ctlptl
|
||||
pkgs.kubernetes-helm
|
||||
pkgs.tilt
|
||||
corepack
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: v1
|
||||
data:
|
||||
Corefile: |
|
||||
.:53 {
|
||||
errors
|
||||
health {
|
||||
lameduck 5s
|
||||
}
|
||||
ready
|
||||
rewrite name speckle.internal ingress-nginx-controller.ingress-nginx.svc.cluster.local.
|
||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||
pods insecure
|
||||
fallthrough in-addr.arpa ip6.arpa
|
||||
ttl 30
|
||||
}
|
||||
prometheus :9153
|
||||
forward . /etc/resolv.conf {
|
||||
max_concurrent 1000
|
||||
}
|
||||
cache 30
|
||||
loop
|
||||
reload
|
||||
loadbalance
|
||||
}
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: coredns
|
||||
namespace: kube-system
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: 'ingress-nginx'
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: 'speckle-dui'
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
kubectl --context="kind-speckle-dui" --namespace="kube-system" rollout restart deployment/coredns
|
||||
kubectl --context="kind-speckle-dui" --namespace="kube-system" rollout status deployment "coredns" --timeout=90s
|
||||
@@ -0,0 +1,7 @@
|
||||
controller:
|
||||
# We must set the kind cluster listen address for every port to '127.0.0.1' when hostNetwork is true
|
||||
hostNetwork: true
|
||||
admissionWebhooks:
|
||||
enabled: false
|
||||
# progressDeadlineSeconds: 600 #HACK helm chart was complaining that this was less than minReadySeconds https://github.com/kubernetes/ingress-nginx/blob/c72441585e1ab1a32df86e760613d36fa804315d/charts/ingress-nginx/templates/controller-deployment.yaml#L26
|
||||
tcp: {}
|
||||
@@ -0,0 +1,16 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations: {}
|
||||
hosts:
|
||||
- host: speckle.internal
|
||||
paths:
|
||||
# Please retain this path, the dashboards expect to serve all paths under the root.
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
security:
|
||||
trustedProxies: []
|
||||
frameAncestors:
|
||||
- "speckle.internal"
|
||||
frameSource:
|
||||
- "speckle.internal"
|
||||
@@ -3552,6 +3552,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@remirror/core-constants@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@remirror/core-constants@npm:3.0.0"
|
||||
checksum: 10c0/15909dd00a2d90cf1f65583bb03ff97c27bb3ec3e22467cdaec3e9cfdae50c687d044df342b985a951d28306cc94cf9188bf7742c7a811ebbb62fd9c5a16ed44
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@repeaterjs/repeater@npm:^3.0.4, @repeaterjs/repeater@npm:^3.0.6":
|
||||
version: 3.0.6
|
||||
resolution: "@repeaterjs/repeater@npm:3.0.6"
|
||||
@@ -4088,6 +4095,204 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/core@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/core@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/pm": ^2.7.0
|
||||
checksum: 10c0/b72a6956720f766bc909954a919870907379875ba41b122db28eadc4e8f67b5b2ebe01cfc9df9dae715ef5d585853b97714c00607514d040ae4fd4d1c2ce70e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-bold@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-bold@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
checksum: 10c0/5c5c168da9a994f77b2e6a0a1924c1d648d3af34c3c36b1fd33ca6a0824fff9192bdc92b5e2433350d641e7efc4b6c40a1f1d5c8b47cb99b05c2d4c6070ed88c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-bubble-menu@npm:^2.10.3":
|
||||
version: 2.27.1
|
||||
resolution: "@tiptap/extension-bubble-menu@npm:2.27.1"
|
||||
dependencies:
|
||||
tippy.js: "npm:^6.3.7"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
"@tiptap/pm": ^2.7.0
|
||||
checksum: 10c0/b7cd0c75be3b93b44f5ed35e752c5db3e98281feaa456013b4016b75fc5a362511e3cd3758b79a32a5bda94cfa0058246ecaf066be617b535d164122121b13dd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-document@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-document@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
checksum: 10c0/7c30c8418f2b78298eb84ea92da3a99889f357937bc9207db3f698f1d83c1cb7f9b54d8c9a87205a998180e4b8229b5170eff3fe72a5f989fabf0a01effb3679
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-floating-menu@npm:^2.10.3":
|
||||
version: 2.27.1
|
||||
resolution: "@tiptap/extension-floating-menu@npm:2.27.1"
|
||||
dependencies:
|
||||
tippy.js: "npm:^6.3.7"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
"@tiptap/pm": ^2.7.0
|
||||
checksum: 10c0/85c1b99fafa2ec16842806c1d05801a747a75b0bb67a216b309ee83e026319c1d9d56bace3d81d5e699757913d4257fa87ffe3c34987f31e040ece22d1dfc36a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-hard-break@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-hard-break@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
checksum: 10c0/0cc805819690ff9f5efa15e5db1d4bb7e604ac859ce46ef93306454a4668e1fa784c0f6fc7e19b5a5ea5d54cffc2a6161a1c5a58836b90db79da2c3f1a9dd729
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-history@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-history@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
"@tiptap/pm": ^2.7.0
|
||||
checksum: 10c0/cb6fd2f8e931d2da14c551679cf1d78522b2fda63d92c93d80a114531be36f34b4b096bfed695b71cb9a348f2e5928a93b2d1c2cd550f086b0afaffb49bc057f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-italic@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-italic@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
checksum: 10c0/8cc067d934dc43813524509482e93f949bdb0428695a0629d3e0f966d0f0382de5f35db0c4e02ec5dec978af403683e15cb6f5ee89eccb417f95a8de09412f26
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-link@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-link@npm:2.10.3"
|
||||
dependencies:
|
||||
linkifyjs: "npm:^4.1.0"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
"@tiptap/pm": ^2.7.0
|
||||
checksum: 10c0/c127fdac8408b9d5a120003c36817b443cae8f0aa1568280007b5d44198dac60af55df44b202f4204be8aefc762a3933c71a864b0af06679b31d326ee589cf15
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-mention@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-mention@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
"@tiptap/pm": ^2.7.0
|
||||
"@tiptap/suggestion": ^2.7.0
|
||||
checksum: 10c0/833e495f611def2619ec4ebd7e47e8714ef756b1ce642aa778336c4be63e56a1a2d1b06c09688e7d6fc3655ddd24d7fb0179bee10e6ceb05ebc5cc3498cc9e87
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-paragraph@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-paragraph@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
checksum: 10c0/4c55fef6600d5ae37b149c3f3bcb1a49ea62e37728e7f6032e2093ad50da34fda1bcc3007f8693749da9dd5ca2929066a29565e61627d987d74a6472a0ca699d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-placeholder@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-placeholder@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
"@tiptap/pm": ^2.7.0
|
||||
checksum: 10c0/9a081ba6b4e3f295633ed2f6cf950891850ea282b24e131f7f2ad942a9f881e337d9757c1df0b7187e39b56c93259b7ade8fc8b46364d8b84067e0491d3b6110
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-strike@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-strike@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
checksum: 10c0/c22eda35bfb9122ed8fa081de03d2e90b7b57174b107401ca3fde67f97968d4d6aa45c0b8218e3c16e3d5df9b0b1c8802bba46a12ca28c45649f98ed480fe7fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-text@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-text@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
checksum: 10c0/4f4514bbf119285f49fef18a67100f845162780aefd0df8fee385c56c3dd0f1be6d4da2a0d9207f21902d8bcb795b87cfff18376738e8de0df540b4b19ecd971
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/extension-underline@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/extension-underline@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
checksum: 10c0/ceb517232ade674da57d360d0ce589911e349ba87c79c9d7cb48cfbe67a28e2c410c0103b379564c3fbc64ed1ec9d3d5b7ef3c364f4bc22d1f890775beb58090
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/pm@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/pm@npm:2.10.3"
|
||||
dependencies:
|
||||
prosemirror-changeset: "npm:^2.2.1"
|
||||
prosemirror-collab: "npm:^1.3.1"
|
||||
prosemirror-commands: "npm:^1.6.2"
|
||||
prosemirror-dropcursor: "npm:^1.8.1"
|
||||
prosemirror-gapcursor: "npm:^1.3.2"
|
||||
prosemirror-history: "npm:^1.4.1"
|
||||
prosemirror-inputrules: "npm:^1.4.0"
|
||||
prosemirror-keymap: "npm:^1.2.2"
|
||||
prosemirror-markdown: "npm:^1.13.1"
|
||||
prosemirror-menu: "npm:^1.2.4"
|
||||
prosemirror-model: "npm:^1.23.0"
|
||||
prosemirror-schema-basic: "npm:^1.2.3"
|
||||
prosemirror-schema-list: "npm:^1.4.1"
|
||||
prosemirror-state: "npm:^1.4.3"
|
||||
prosemirror-tables: "npm:^1.6.1"
|
||||
prosemirror-trailing-node: "npm:^3.0.0"
|
||||
prosemirror-transform: "npm:^1.10.2"
|
||||
prosemirror-view: "npm:^1.37.0"
|
||||
checksum: 10c0/8da579aa1e052f056cee7765dafb3a00bed2a9e110e0db01225d0e6d3a3e701ff4bc58e5bd13cc48eb946370edabaa6f19d6fcf33aa790fad3b4845f99fdfd56
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/suggestion@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/suggestion@npm:2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
"@tiptap/pm": ^2.7.0
|
||||
checksum: 10c0/552e0842dce3feb088e28bbcfe936ce4c4b041dcd593eba09a51a83bc1824a2605e27e6b5251b8d0edc3db798975637ea09776427510d76a836dd68744d22a38
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tiptap/vue-3@npm:2.10.3":
|
||||
version: 2.10.3
|
||||
resolution: "@tiptap/vue-3@npm:2.10.3"
|
||||
dependencies:
|
||||
"@tiptap/extension-bubble-menu": "npm:^2.10.3"
|
||||
"@tiptap/extension-floating-menu": "npm:^2.10.3"
|
||||
peerDependencies:
|
||||
"@tiptap/core": ^2.7.0
|
||||
"@tiptap/pm": ^2.7.0
|
||||
vue: ^3.0.0
|
||||
checksum: 10c0/10eff4df3450bf672ef8ee9ce9f5679df6a920f21418ad55c4fdbc2b83827ab3a0aca6538f35ae7bfb56351f51740a645d9f87a5d61db0f6d61c347b24c787fc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@trysound/sax@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@trysound/sax@npm:0.2.0"
|
||||
@@ -4178,6 +4383,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/linkify-it@npm:^5":
|
||||
version: 5.0.0
|
||||
resolution: "@types/linkify-it@npm:5.0.0"
|
||||
checksum: 10c0/7bbbf45b9dde17bf3f184fee585aef0e7342f6954f0377a24e4ff42ab5a85d5b806aaa5c8d16e2faf2a6b87b2d94467a196b7d2b85c9c7de2f0eaac5487aaab8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash-es@npm:^4.17.6":
|
||||
version: 4.17.12
|
||||
resolution: "@types/lodash-es@npm:4.17.12"
|
||||
@@ -4194,6 +4406,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/markdown-it@npm:^14.0.0":
|
||||
version: 14.1.2
|
||||
resolution: "@types/markdown-it@npm:14.1.2"
|
||||
dependencies:
|
||||
"@types/linkify-it": "npm:^5"
|
||||
"@types/mdurl": "npm:^2"
|
||||
checksum: 10c0/34f709f0476bd4e7b2ba7c3341072a6d532f1f4cb6f70aef371e403af8a08a7c372ba6907ac426bc618d356dab660c5b872791ff6c1ead80c483e0d639c6f127
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mdurl@npm:^2":
|
||||
version: 2.0.0
|
||||
resolution: "@types/mdurl@npm:2.0.0"
|
||||
checksum: 10c0/cde7bb571630ed1ceb3b92a28f7b59890bb38b8f34cd35326e2df43eebfc74985e6aa6fd4184e307393bad8a9e0783a519a3f9d13c8e03788c0f98e5ec869c5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*, @types/node@npm:>=10.0.0":
|
||||
version: 22.15.17
|
||||
resolution: "@types/node@npm:22.15.17"
|
||||
@@ -6863,6 +7092,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"crelt@npm:^1.0.0":
|
||||
version: 1.0.6
|
||||
resolution: "crelt@npm:1.0.6"
|
||||
checksum: 10c0/e0fb76dff50c5eb47f2ea9b786c17f9425c66276025adee80876bdbf4a84ab72e899e56d3928431ab0cb057a105ef704df80fe5726ef0f7b1658f815521bdf09
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cron-parser@npm:^4.9.0":
|
||||
version: 4.9.0
|
||||
resolution: "cron-parser@npm:4.9.0"
|
||||
@@ -10827,6 +11063,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"linkify-it@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "linkify-it@npm:5.0.0"
|
||||
dependencies:
|
||||
uc.micro: "npm:^2.0.0"
|
||||
checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"linkifyjs@npm:^4.1.0":
|
||||
version: 4.3.2
|
||||
resolution: "linkifyjs@npm:4.3.2"
|
||||
checksum: 10c0/1a85e6b368304a4417567fe5e38651681e3e82465590836942d1b4f3c834cc35532898eb1e2479f6337d9144b297d418eb708b6be8ed0b3dc3954a3588e07971
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listhen@npm:^1.5.6, listhen@npm:^1.9.0":
|
||||
version: 1.9.0
|
||||
resolution: "listhen@npm:1.9.0"
|
||||
@@ -11113,6 +11365,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lucide-vue-next@npm:^0.537.0":
|
||||
version: 0.537.0
|
||||
resolution: "lucide-vue-next@npm:0.537.0"
|
||||
peerDependencies:
|
||||
vue: ">=3.0.1"
|
||||
checksum: 10c0/5004c551894c19394ec15340ebb588e64ab7e237a12de713780b56b58f82cd473cfd2007e33a5a6d678f513aa559a4f84a0375321466f4e465e9755d585df877
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"luxon@npm:^3.2.1":
|
||||
version: 3.6.1
|
||||
resolution: "luxon@npm:3.6.1"
|
||||
@@ -11193,6 +11454,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"markdown-it@npm:^14.0.0":
|
||||
version: 14.1.0
|
||||
resolution: "markdown-it@npm:14.1.0"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
entities: "npm:^4.4.0"
|
||||
linkify-it: "npm:^5.0.0"
|
||||
mdurl: "npm:^2.0.0"
|
||||
punycode.js: "npm:^2.3.1"
|
||||
uc.micro: "npm:^2.1.0"
|
||||
bin:
|
||||
markdown-it: bin/markdown-it.mjs
|
||||
checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"matcher@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "matcher@npm:3.0.0"
|
||||
@@ -11237,6 +11514,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdurl@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdurl@npm:2.0.0"
|
||||
checksum: 10c0/633db522272f75ce4788440669137c77540d74a83e9015666a9557a152c02e245b192edc20bc90ae953bbab727503994a53b236b4d9c99bdaee594d0e7dd2ce0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"media-typer@npm:0.3.0":
|
||||
version: 0.3.0
|
||||
resolution: "media-typer@npm:0.3.0"
|
||||
@@ -12404,6 +12688,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"orderedmap@npm:^2.0.0":
|
||||
version: 2.1.1
|
||||
resolution: "orderedmap@npm:2.1.1"
|
||||
checksum: 10c0/8d7d266659d1828937046e8b2a7b5f75914e0391db985da0ca75cd2246cccbf6d6f3a0886aa2034da15ee4923e8c45f95f8b588f575f535f0adecdefccc54634
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"os-tmpdir@npm:~1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "os-tmpdir@npm:1.0.2"
|
||||
@@ -13592,6 +13883,200 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-changeset@npm:^2.2.1":
|
||||
version: 2.3.1
|
||||
resolution: "prosemirror-changeset@npm:2.3.1"
|
||||
dependencies:
|
||||
prosemirror-transform: "npm:^1.0.0"
|
||||
checksum: 10c0/efd6578ee4535d72d11c032b49921f14b3f7ccae680eb14c8d9f6cc1fbec00299c598475af0ab432864976bdbb7f94f011193278b2d19eadda83b754fe6d8a35
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-collab@npm:^1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "prosemirror-collab@npm:1.3.1"
|
||||
dependencies:
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
checksum: 10c0/5d7553c136929cfd847b8781be599561d0f21e78fae80d930eb5f1d4d644307bc779cdfaeae86dd31a8be8f562c28dee19f1a06a2900e9b591b02957151fe90c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-commands@npm:^1.0.0, prosemirror-commands@npm:^1.6.2":
|
||||
version: 1.7.1
|
||||
resolution: "prosemirror-commands@npm:1.7.1"
|
||||
dependencies:
|
||||
prosemirror-model: "npm:^1.0.0"
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
prosemirror-transform: "npm:^1.10.2"
|
||||
checksum: 10c0/4884ea7a66b79b51e72bb2ef358284d70e9a071deb4cbfab3dd8ee3449e9a0e34cb391d92f487c013d3716b823fc5568ad5e409a9444b3630ae0b87617c2fca1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-dropcursor@npm:^1.8.1":
|
||||
version: 1.8.2
|
||||
resolution: "prosemirror-dropcursor@npm:1.8.2"
|
||||
dependencies:
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
prosemirror-transform: "npm:^1.1.0"
|
||||
prosemirror-view: "npm:^1.1.0"
|
||||
checksum: 10c0/c3d9e456a64fecc77a6e6a0350116598550dee8cb55f74e8b66fdb26150c48340ddd1f43184134b24d0f2e710b6879ff6ec72c215dc618a6a673320a91c90478
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-gapcursor@npm:^1.3.2":
|
||||
version: 1.4.0
|
||||
resolution: "prosemirror-gapcursor@npm:1.4.0"
|
||||
dependencies:
|
||||
prosemirror-keymap: "npm:^1.0.0"
|
||||
prosemirror-model: "npm:^1.0.0"
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
prosemirror-view: "npm:^1.0.0"
|
||||
checksum: 10c0/c9f8274198642ca37209338d4222099d7d50c4dd743c8aab25703e2845c6ced691ca9368ad6ac2aaa1899b51709e7411c5fbada306c58301df82f168fbc4517e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-history@npm:^1.0.0, prosemirror-history@npm:^1.4.1":
|
||||
version: 1.5.0
|
||||
resolution: "prosemirror-history@npm:1.5.0"
|
||||
dependencies:
|
||||
prosemirror-state: "npm:^1.2.2"
|
||||
prosemirror-transform: "npm:^1.0.0"
|
||||
prosemirror-view: "npm:^1.31.0"
|
||||
rope-sequence: "npm:^1.3.0"
|
||||
checksum: 10c0/9f24c99316c30a52ff40ddd59fc83b5180f1326ee6f466bfa82b847a503b0cdff234c35d5e3decb39ae1382a189ceec7462333ed7d6c34e2c95921892bb98b78
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-inputrules@npm:^1.4.0":
|
||||
version: 1.5.1
|
||||
resolution: "prosemirror-inputrules@npm:1.5.1"
|
||||
dependencies:
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
prosemirror-transform: "npm:^1.0.0"
|
||||
checksum: 10c0/cff1ff9f7e726bf324e6cddd2c48984e6b2970cc98cd5dc59e6dbe2e9df0e01e4a2d100c77c721067804170b9607769c0fee7c98f2cfb4f3cff9af919fce31b9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-keymap@npm:^1.0.0, prosemirror-keymap@npm:^1.2.2, prosemirror-keymap@npm:^1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "prosemirror-keymap@npm:1.2.3"
|
||||
dependencies:
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
w3c-keyname: "npm:^2.2.0"
|
||||
checksum: 10c0/0ec2f8bd9b608d0e6a0cdab1d66f9a6b41edcff0239b32ccca1018a0733e52448e4758218a2d472fb8c33c1609426dc6bad4944b28c1c3d509a83201a23035e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-markdown@npm:^1.13.1":
|
||||
version: 1.13.2
|
||||
resolution: "prosemirror-markdown@npm:1.13.2"
|
||||
dependencies:
|
||||
"@types/markdown-it": "npm:^14.0.0"
|
||||
markdown-it: "npm:^14.0.0"
|
||||
prosemirror-model: "npm:^1.25.0"
|
||||
checksum: 10c0/53c48ef0d0d18ca0a7c39bdb18485508bfe10582f9b1d04d7114e6f6e9678a4481b318f310b19d4e95f65d947fbe6348affddcb909ad9b8c9f865cc07ceff22b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-menu@npm:^1.2.4":
|
||||
version: 1.2.5
|
||||
resolution: "prosemirror-menu@npm:1.2.5"
|
||||
dependencies:
|
||||
crelt: "npm:^1.0.0"
|
||||
prosemirror-commands: "npm:^1.0.0"
|
||||
prosemirror-history: "npm:^1.0.0"
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
checksum: 10c0/a4da649aa3c7bfb74128da203984009b44fd48638ff76ec7b209635fafd23b05d7d5bed9520282cdcf886f73eafcfbda4e77f55d81a92db333f8807d84ded2f9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-model@npm:^1.0.0, prosemirror-model@npm:^1.20.0, prosemirror-model@npm:^1.21.0, prosemirror-model@npm:^1.23.0, prosemirror-model@npm:^1.25.0, prosemirror-model@npm:^1.25.4":
|
||||
version: 1.25.4
|
||||
resolution: "prosemirror-model@npm:1.25.4"
|
||||
dependencies:
|
||||
orderedmap: "npm:^2.0.0"
|
||||
checksum: 10c0/5ba99a235497df3452c0e2dfb71ee05d898fb9cb539c2a92583524fc4f337d8abab5c6b49b3af242c86327ea27cce41c7169a1e0c7bb4f7ef1502b311bd00cfc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-schema-basic@npm:^1.2.3":
|
||||
version: 1.2.4
|
||||
resolution: "prosemirror-schema-basic@npm:1.2.4"
|
||||
dependencies:
|
||||
prosemirror-model: "npm:^1.25.0"
|
||||
checksum: 10c0/cd86f88a5eb51ab5459aa91e6824e73ec15b0f1546fee89be7826663663ef11eefaacacda5a14c43b4c8d8477fd653642418b9c7d485bb92e323f9b8e7607a78
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-schema-list@npm:^1.4.1":
|
||||
version: 1.5.1
|
||||
resolution: "prosemirror-schema-list@npm:1.5.1"
|
||||
dependencies:
|
||||
prosemirror-model: "npm:^1.0.0"
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
prosemirror-transform: "npm:^1.7.3"
|
||||
checksum: 10c0/e6fd27446bc90556a9797f6ca0cb54e7db53cc7c20fbf633b7d0f4709c45accfa2f3a0f6575fe47aa83cb75781a9b773198d236a44db9d8eef2802a1501e4301
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-state@npm:^1.0.0, prosemirror-state@npm:^1.2.2, prosemirror-state@npm:^1.4.3, prosemirror-state@npm:^1.4.4":
|
||||
version: 1.4.4
|
||||
resolution: "prosemirror-state@npm:1.4.4"
|
||||
dependencies:
|
||||
prosemirror-model: "npm:^1.0.0"
|
||||
prosemirror-transform: "npm:^1.0.0"
|
||||
prosemirror-view: "npm:^1.27.0"
|
||||
checksum: 10c0/1428636a37c127afe0d11a4f4eb44d75a7f16717940405082bd16fa4c28cfeef9375d49f48e411e5f0fa26f3e5798af7f270edc9bc9b0e647df8bb79983bcd59
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-tables@npm:^1.6.1":
|
||||
version: 1.8.3
|
||||
resolution: "prosemirror-tables@npm:1.8.3"
|
||||
dependencies:
|
||||
prosemirror-keymap: "npm:^1.2.3"
|
||||
prosemirror-model: "npm:^1.25.4"
|
||||
prosemirror-state: "npm:^1.4.4"
|
||||
prosemirror-transform: "npm:^1.10.5"
|
||||
prosemirror-view: "npm:^1.41.4"
|
||||
checksum: 10c0/3c1a7ca0684b9182159af679173dde6cbf3c5af4dd3f2278712b0f33d5d065d014a913d0897881e44c82204d57b89c52cb8876d66f2737a36519f092c9613fed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-trailing-node@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "prosemirror-trailing-node@npm:3.0.0"
|
||||
dependencies:
|
||||
"@remirror/core-constants": "npm:3.0.0"
|
||||
escape-string-regexp: "npm:^4.0.0"
|
||||
peerDependencies:
|
||||
prosemirror-model: ^1.22.1
|
||||
prosemirror-state: ^1.4.2
|
||||
prosemirror-view: ^1.33.8
|
||||
checksum: 10c0/d512054543a872c667bcd661f207c54a38287a8e62a2ff4aa87d65aefbad0bf3a6315cc7531d9c63cc7a7ef93504966b6c9496af90287a710914688feba72454
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-transform@npm:^1.0.0, prosemirror-transform@npm:^1.1.0, prosemirror-transform@npm:^1.10.2, prosemirror-transform@npm:^1.10.5, prosemirror-transform@npm:^1.7.3":
|
||||
version: 1.10.5
|
||||
resolution: "prosemirror-transform@npm:1.10.5"
|
||||
dependencies:
|
||||
prosemirror-model: "npm:^1.21.0"
|
||||
checksum: 10c0/64e5aeaa30f15a2873214913b3fda6fe9973802e6291d5913a549c71bf6de3e167ab0d967ee880041e3fecdcb3d950388346d805f21b3a1b6ec77285c17f40bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prosemirror-view@npm:^1.0.0, prosemirror-view@npm:^1.1.0, prosemirror-view@npm:^1.27.0, prosemirror-view@npm:^1.31.0, prosemirror-view@npm:^1.37.0, prosemirror-view@npm:^1.41.4":
|
||||
version: 1.41.4
|
||||
resolution: "prosemirror-view@npm:1.41.4"
|
||||
dependencies:
|
||||
prosemirror-model: "npm:^1.20.0"
|
||||
prosemirror-state: "npm:^1.0.0"
|
||||
prosemirror-transform: "npm:^1.1.0"
|
||||
checksum: 10c0/613e36cb27757c115ab301d3f7674e979450c928064b95fe26666765a2fc3efa80402ca8f6e4b8484e5d2f6945a14d842e1ff2900c86e4deea73613be0f42d5a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proto-list@npm:~1.2.1":
|
||||
version: 1.2.4
|
||||
resolution: "proto-list@npm:1.2.4"
|
||||
@@ -13626,6 +14111,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"punycode.js@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "punycode.js@npm:2.3.1"
|
||||
checksum: 10c0/1d12c1c0e06127fa5db56bd7fdf698daf9a78104456a6b67326877afc21feaa821257b171539caedd2f0524027fa38e67b13dd094159c8d70b6d26d2bea4dfdb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"punycode@npm:^2.1.0":
|
||||
version: 2.3.1
|
||||
resolution: "punycode@npm:2.3.1"
|
||||
@@ -14296,6 +14788,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rope-sequence@npm:^1.3.0":
|
||||
version: 1.3.4
|
||||
resolution: "rope-sequence@npm:1.3.4"
|
||||
checksum: 10c0/caa90be3d7a7cad155fb354a4679a1280dc9819c81bd319542a0d893a64e152284abb9cc1631d4351b328016a8d6c35a48c912234edfaf5173daef44b2e3609b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"router@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "router@npm:2.2.0"
|
||||
@@ -14966,6 +15465,22 @@ __metadata:
|
||||
"@speckle/tailwind-theme": "npm:2.25.0"
|
||||
"@speckle/ui-components": "npm:^2.25.0"
|
||||
"@speckle/ui-components-nuxt": "npm:^2.25.0"
|
||||
"@tiptap/core": "npm:2.10.3"
|
||||
"@tiptap/extension-bold": "npm:2.10.3"
|
||||
"@tiptap/extension-document": "npm:2.10.3"
|
||||
"@tiptap/extension-hard-break": "npm:2.10.3"
|
||||
"@tiptap/extension-history": "npm:2.10.3"
|
||||
"@tiptap/extension-italic": "npm:2.10.3"
|
||||
"@tiptap/extension-link": "npm:2.10.3"
|
||||
"@tiptap/extension-mention": "npm:2.10.3"
|
||||
"@tiptap/extension-paragraph": "npm:2.10.3"
|
||||
"@tiptap/extension-placeholder": "npm:2.10.3"
|
||||
"@tiptap/extension-strike": "npm:2.10.3"
|
||||
"@tiptap/extension-text": "npm:2.10.3"
|
||||
"@tiptap/extension-underline": "npm:2.10.3"
|
||||
"@tiptap/pm": "npm:2.10.3"
|
||||
"@tiptap/suggestion": "npm:2.10.3"
|
||||
"@tiptap/vue-3": "npm:2.10.3"
|
||||
"@types/apollo-upload-client": "npm:^17.0.1"
|
||||
"@types/eslint": "npm:^9.6.1"
|
||||
"@types/lodash-es": "npm:^4.17.6"
|
||||
@@ -14988,6 +15503,7 @@ __metadata:
|
||||
graphql: "npm:^16.6.0"
|
||||
graphql-tag: "npm:^2.12.6"
|
||||
lodash-es: "npm:^4.17.21"
|
||||
lucide-vue-next: "npm:^0.537.0"
|
||||
nanoevents: "npm:^8.0.0"
|
||||
nuxt: "npm:^3.17.3"
|
||||
pinia: "npm:^2.1.4"
|
||||
@@ -16088,6 +16604,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "uc.micro@npm:2.1.0"
|
||||
checksum: 10c0/8862eddb412dda76f15db8ad1c640ccc2f47cdf8252a4a30be908d535602c8d33f9855dfcccb8b8837855c1ce1eaa563f7fa7ebe3c98fd0794351aab9b9c55fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ufo@npm:^1.1.2, ufo@npm:^1.3.2, ufo@npm:^1.5.4, ufo@npm:^1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "ufo@npm:1.6.1"
|
||||
@@ -17051,6 +17574,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"w3c-keyname@npm:^2.2.0":
|
||||
version: 2.2.8
|
||||
resolution: "w3c-keyname@npm:2.2.8"
|
||||
checksum: 10c0/37cf335c90efff31672ebb345577d681e2177f7ff9006a9ad47c68c5a9d265ba4a7b39d6c2599ceea639ca9315584ce4bd9c9fbf7a7217bfb7a599e71943c4c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"wcwidth@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "wcwidth@npm:1.0.1"
|
||||
|
||||
Reference in New Issue
Block a user