diff --git a/.circleci/config.yml b/.circleci/config.yml
index 624c3376d..1d019b235 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -23,6 +23,17 @@ workflows:
# run tests for any commit on any branch, including any tags
only: /.*/
+ - test-frontend-2:
+ filters: &filters-frontend-2
+ tags:
+ # frontend-2 prefix only
+ only: /^frontend\-2.*/
+
+ - chromatic:
+ context:
+ - chromatic
+ filters: *filters-frontend-2
+
- get-version:
filters: *filters-allow-all
@@ -45,6 +56,11 @@ workflows:
requires:
- get-version
+ - docker-build-frontend-2:
+ filters: *filters-build
+ requires:
+ - get-version
+
- docker-build-webhooks:
context: *build-context
filters: *filters-build
@@ -83,7 +99,9 @@ workflows:
type: approval
filters: &filters-ignore-main-branch-or-all-tags
branches:
- ignore: main
+ ignore:
+ - main
+ - frontend-2
tags:
ignore: /.*/
@@ -110,6 +128,16 @@ workflows:
- docker-build-frontend
- pre-commit
+ - docker-publish-frontend-2:
+ context: *docker-hub-context
+ filters: *filters-publish
+ requires:
+ - get-version
+ - publish-approval
+ - docker-build-frontend-2
+ - pre-commit
+ - test-frontend-2
+
- docker-publish-webhooks:
context: *docker-hub-context
filters: *filters-publish
@@ -161,6 +189,7 @@ workflows:
only:
- main
- hotfix*
+ - frontend-2
tags:
only: &filters-tag /^[0-9]+\.[0-9]+\.[0-9]+$/
requires:
@@ -168,6 +197,7 @@ workflows:
- publish-approval
- docker-publish-server
- docker-publish-frontend
+ - docker-publish-frontend-2
- docker-publish-webhooks
- docker-publish-file-imports
- docker-publish-previews
@@ -249,6 +279,9 @@ jobs:
paths:
- .yarn/cache
- .yarn/unplugged
+ - run:
+ name: Build public packages
+ command: yarn build:public
- run:
name: Run pre-commit
command: ./.husky/pre-commit
@@ -294,7 +327,6 @@ jobs:
- run:
name: Install Dependencies
command: yarn
- # working_directory: 'packages/server'
- save_cache:
name: Save Yarn Package Cache
@@ -335,6 +367,75 @@ jobs:
# path: packages/server/coverage/lcov-report
# destination: package/server/coverage
+ test-frontend-2:
+ docker:
+ - image: cimg/node:16.15-browsers
+ resource_class: xlarge
+ steps:
+ - checkout
+ - restore_cache:
+ name: Restore Yarn Package Cache
+ keys:
+ - yarn-packages-server-{{ checksum "yarn.lock" }}
+ - run:
+ name: Install Dependencies
+ command: yarn
+
+ - save_cache:
+ name: Save Yarn Package Cache
+ key: yarn-packages-server-{{ checksum "yarn.lock" }}
+ paths:
+ - .yarn/cache
+ - .yarn/unplugged
+
+ - run:
+ name: Build public packages
+ command: yarn build:public
+
+ - run:
+ name: Lint everything
+ command: yarn lint
+ working_directory: 'packages/frontend-2'
+
+ - run:
+ name: Install Playwright
+ command: cd ~ && npx playwright install --with-deps
+
+ - run:
+ name: Test via Storybook
+ command: yarn storybook:test:ci
+ working_directory: 'packages/frontend-2'
+
+ chromatic:
+ resource_class: medium+
+ docker:
+ - image: cimg/node:16.15
+ steps:
+ - checkout
+ - restore_cache:
+ name: Restore Yarn Package Cache
+ keys:
+ - yarn-packages-server-{{ checksum "yarn.lock" }}
+ - run:
+ name: Install Dependencies
+ command: yarn
+
+ - save_cache:
+ name: Save Yarn Package Cache
+ key: yarn-packages-server-{{ checksum "yarn.lock" }}
+ paths:
+ - .yarn/cache
+ - .yarn/unplugged
+
+ - run:
+ name: Build shared packages
+ command: yarn build:public
+
+ - run:
+ name: Run chromatic
+ command: yarn chromatic
+ working_directory: 'packages/frontend-2'
+
vulnerability-scan:
# snyk can undertake most types of scans through GitHub integration
# which does not require integration with the CI
@@ -384,6 +485,7 @@ jobs:
command: |
[[ "${CIRCLE_TAG}" ]] && echo "proceed because tag is set" && exit 0
[[ "${CIRCLE_BRANCH}" == "main" ]] && echo "proceed because main branch" && exit 0
+ [[ "${CIRCLE_BRANCH}" == "frontend-2" ]] && echo "proceed because frontend-2 branch" && exit 0
[[ "${IS_DRAFT_PR}" == "TRUE" || -z "${CIRCLE_PULL_REQUEST}" ]] && echo "Should not build because either Draft PR or branch without PR, stopping" && exit 1
echo "proceeding"
- setup_remote_docker:
@@ -409,6 +511,11 @@ jobs:
environment:
SPECKLE_SERVER_PACKAGE: frontend
+ docker-build-frontend-2:
+ <<: *build-job
+ environment:
+ SPECKLE_SERVER_PACKAGE: frontend-2
+
docker-build-previews:
<<: *build-job
environment:
@@ -465,6 +572,11 @@ jobs:
environment:
SPECKLE_SERVER_PACKAGE: frontend
+ docker-publish-frontend-2:
+ <<: *publish-job
+ environment:
+ SPECKLE_SERVER_PACKAGE: frontend-2
+
docker-publish-previews:
<<: *publish-job
environment:
diff --git a/.circleci/publish.sh b/.circleci/publish.sh
index b37ab577f..fd2c06c4e 100755
--- a/.circleci/publish.sh
+++ b/.circleci/publish.sh
@@ -26,7 +26,7 @@ if [[ "${IMAGE_VERSION_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-alpha\.[0-9]+)?$ ]]; th
docker push "${DOCKER_IMAGE_TAG}:2"
fi
else
- BRANCH_TAG="${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}"
+ BRANCH_TAG="branch.${BRANCH_NAME_TRUNCATED}"
echo "🏷 Tagging and pushing image as '${DOCKER_IMAGE_TAG}:${BRANCH_TAG}'"
docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:${BRANCH_TAG}"
docker push "${DOCKER_IMAGE_TAG}:${BRANCH_TAG}"
diff --git a/.circleci/publish_helm_chart.sh b/.circleci/publish_helm_chart.sh
index 1ddcde445..6cabffd9d 100755
--- a/.circleci/publish_helm_chart.sh
+++ b/.circleci/publish_helm_chart.sh
@@ -1,39 +1,51 @@
-#!/bin/bash
+#!/usr/bin/env bash
-set -e
+set -eo pipefail
-RELEASE_VERSION=${IMAGE_VERSION_TAG}
+GIT_REPO=$( pwd )
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+# shellcheck disable=SC1090,SC1091
+source "${SCRIPT_DIR}/common.sh"
-echo "Releasing Helm Chart version $RELEASE_VERSION"
+RELEASE_VERSION="${IMAGE_VERSION_TAG}"
+HELM_STABLE_BRANCH="${HELM_STABLE_BRANCH:-"main"}"
+
+echo "Releasing Helm Chart version ${RELEASE_VERSION}"
git config --global user.email "devops+circleci@speckle.systems"
git config --global user.name "CI"
-git clone git@github.com:specklesystems/helm.git ~/helm
-# before overwriting the chart with the build version, check if the current chart version
-# is not newer than the currently build one
+git clone git@github.com:specklesystems/helm.git "${HOME}/helm"
-CURRENT_VERSION="$(grep ^version ~/helm/charts/speckle-server/Chart.yaml | grep -o '2\..*')"
-echo "${CURRENT_VERSION}"
+yq e -i ".version = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml"
+yq e -i ".appVersion = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml"
+yq e -i ".docker_image_tag = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/values.yaml"
-.circleci/check_version.py "${CURRENT_VERSION}" "${RELEASE_VERSION}"
-if [ $? -eq 1 ]
-then
- echo "The current helm chart version is newer than the currently built. Exiting"
- exit 1
+if [[ -n "${CIRCLE_TAG}" || "${CIRCLE_BRANCH}" == "${HELM_STABLE_BRANCH}" ]]; then
+ # before overwriting the chart with the build version, check if the current chart version
+ # is not newer than the currently build one
+
+ CURRENT_VERSION="$(grep ^version "${HOME}/helm/charts/speckle-server/Chart.yaml" | grep -o '2\..*')"
+ echo "${CURRENT_VERSION}"
+
+ .circleci/check_version.py "${CURRENT_VERSION}" "${RELEASE_VERSION}"
+ if [ $? -eq 1 ]
+ then
+ echo "The current helm chart version '${CURRENT_VERSION}' is newer than the version '${RELEASE_VERSION}' we are attempting to publish. Exiting"
+ exit 1
+ fi
+ rm -rf "${HOME}/helm/charts/speckle-server"
+ cp -r "${GIT_REPO}/utils/helm/speckle-server" "${HOME}/helm/charts/speckle-server"
+else
+ # overwrite the name of the chart
+ yq e -i ".name = \"speckle-server-branch-${BRANCH_NAME_TRUNCATED}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml"
+ rm -rf "${HOME}/helm/charts/speckle-server-branch-${BRANCH_NAME_TRUNCATED}"
+ cp -r "${GIT_REPO}/utils/helm/speckle-server" "${HOME}/helm/charts/speckle-server-branch-${BRANCH_NAME_TRUNCATED}"
fi
-rm -rf ~/helm/charts/speckle-server
-cp -r utils/helm/speckle-server ~/helm/charts/speckle-server
-
-sed -i 's/version: [^\s]*/version: '"${RELEASE_VERSION}"'/g' ~/helm/charts/speckle-server/Chart.yaml
-sed -i 's/appVersion: [^\s]*/appVersion: '\""${RELEASE_VERSION}"\"'/g' ~/helm/charts/speckle-server/Chart.yaml
-
-sed -i 's/docker_image_tag: [^\s]*/docker_image_tag: '"${RELEASE_VERSION}"'/g' ~/helm/charts/speckle-server/values.yaml
-
cd ~/helm
git add .
-git commit -m "CircleCI commit"
+git commit -m "CircleCI commit for version '${RELEASE_VERSION}'"
git push
diff --git a/.eslintrc.js b/.eslintrc.js
index 5963f699a..3ca47ba4e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -23,10 +23,13 @@ const config = {
ignorePatterns: [
'node_modules',
'dist',
+ 'dist-*',
'public',
'events.json',
'.*.{ts,js,vue,tsx,jsx}',
- 'generated/**/*'
+ 'generated/**/*',
+ '.nuxt',
+ '.output'
]
}
diff --git a/.gitignore b/.gitignore
index 7c88eeefb..d60ba0030 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,5 @@ packages/server/.vscode/*.log
# GitGuardian
.cache_ggshield
+
+storybook-static
diff --git a/.graphqlrc b/.graphqlrc
index 3a4356aa2..0c330da9f 100644
--- a/.graphqlrc
+++ b/.graphqlrc
@@ -1,4 +1,8 @@
schema: 'http://localhost:3000/graphql'
+extensions:
+ languageService:
+ # Cause it's busted
+ enableValidation: false
require:
- ts-node/register
- tsconfig-paths/register
diff --git a/.prettierignore b/.prettierignore
index d8a38369c..25b394b75 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,11 +2,19 @@ node_modules
build
dist
dist2
+dist-*
coverage
.nyc_output
packages/server/reports*
+packages/preview-service/public/render/**/*
packages/objectloader/examples/browser/objectloader.web.js
packages/viewer/example/speckleviewer.web.js
+
+packages/frontend-2/.output
+packages/frontend-2/.nuxt
+packages/frontend-2/lib/core/nuxt-modules/**/templates/*.js
+packages/frontend-2/lib/common/generated/**/*
+
package-lock.json
yarn.lock
.yarn
@@ -24,4 +32,6 @@ venv
.*.{ts,js,vue,tsx,jsx}
**/generated/**/*
-**/generated/graphql.ts
\ No newline at end of file
+**/generated/graphql.ts
+
+storybook-static
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 1c1e0ef97..923d80eeb 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -8,7 +8,8 @@
"esbenp.prettier-vscode",
"hbenl.vscode-mocha-test-adapter",
"ryanluker.vscode-coverage-gutters",
- "Vue.volar"
+ "Vue.volar",
+ "bradlc.vscode-tailwindcss"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": ["octref.vetur", "vscode.typescript-language-features"]
diff --git a/lint-staged.config.js b/lint-staged.config.js
index 31cc62749..ad2cdd54b 100644
--- a/lint-staged.config.js
+++ b/lint-staged.config.js
@@ -9,7 +9,9 @@ module.exports = {
// Filter out generated folder files
`**/generated/**/*`,
// Filter out types in object loader
- '**/packages/objectloader/types/**/*'
+ '**/packages/objectloader/types/**/*',
+ // Filter out nuxt plugin templates
+ '**/templates/plugin.js'
])
return 'eslint --cache --max-warnings=0 ' + finalFiles.join(' ')
diff --git a/package.json b/package.json
index df480b60d..be591a521 100644
--- a/package.json
+++ b/package.json
@@ -22,9 +22,10 @@
"dev": "yarn workspaces foreach -piv -j unlimited run dev",
"dev:no-server": "yarn workspaces foreach --exclude @speckle/server -piv -j unlimited run dev",
"dev:minimal": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend,@speckle/shared}' run dev",
- "gqlgen": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend}' run gqlgen",
+ "gqlgen": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend,@speckle/frontend-2}' run gqlgen",
"dev:server": "yarn workspace @speckle/server dev",
"dev:frontend": "yarn workspace @speckle/frontend dev",
+ "dev:frontend-2": "yarn workspace @speckle/frontend-2 dev",
"dev:shared": "yarn workspace @speckle/shared dev",
"prepare": "husky install",
"postinstall": "husky install",
@@ -48,7 +49,13 @@
"tslib": "^2.3.1",
"core-js": "3.22.4",
"graphql": "^15.3.0",
- "typescript": "^4.8.4"
+ "typescript": "^5.0.4",
+ "prettier": "^2.8.7",
+ "@typescript-eslint/eslint-plugin": "^5.59.0",
+ "@typescript-eslint/parser": "^5.59.0",
+ "eslint": "^8.11.0",
+ "eslint-config-prettier": "^8.5.0",
+ "@types/react": "file:./packages/frontend-2/type-augmentations/stubs/types__react"
},
"config": {
"commitizen": {
diff --git a/packages/fileimport-service/src/daemon.js b/packages/fileimport-service/src/daemon.js
index fdea34d29..b27cff040 100644
--- a/packages/fileimport-service/src/daemon.js
+++ b/packages/fileimport-service/src/daemon.js
@@ -57,12 +57,16 @@ async function doTask(task) {
let fileSizeForMetric = 0
const metricDurationEnd = metricDuration.startTimer()
+ let newBranchCreated = false
+ let branchMetadata = { streamId: null, branchName: null }
+
try {
taskLogger.info('Doing task.')
const info = await FileUploads().where({ id: task.id }).first()
if (!info) {
throw new Error('Internal error: DB inconsistent')
}
+
fileTypeForMetric = info.fileType || 'missing_info'
fileSizeForMetric = Number(info.fileSize) || 0
taskLogger = taskLogger.child({
@@ -77,6 +81,19 @@ async function doTask(task) {
fs.mkdirSync(TMP_INPUT_DIR, { recursive: true })
serverApi = new ServerAPI({ streamId: info.streamId })
+
+ branchMetadata = {
+ branchName: info.branchName,
+ streamId: info.streamId
+ }
+ const existingBranch = await serverApi.getBranchByNameAndStreamId({
+ streamId: info.streamId,
+ name: info.branchName
+ })
+ if (!existingBranch) {
+ newBranchCreated = true
+ }
+
const { token } = await serverApi.createToken({
userId: info.userId,
name: 'temp upload token',
@@ -189,6 +206,13 @@ async function doTask(task) {
[err.toString(), task.id]
)
metricOperationErrors.labels(fileTypeForMetric).inc()
+ } finally {
+ const { streamId, branchName } = branchMetadata
+ await knex.raw(
+ `NOTIFY file_import_update, '${task.id}:::${streamId}:::${branchName}:::${
+ newBranchCreated ? 1 : 0
+ }'`
+ )
}
metricDurationEnd({ op: fileTypeForMetric })
metricInputFileSize.labels(fileTypeForMetric).observe(fileSizeForMetric)
diff --git a/packages/frontend-2/.env.example b/packages/frontend-2/.env.example
new file mode 100644
index 000000000..c26379cba
--- /dev/null
+++ b/packages/frontend-2/.env.example
@@ -0,0 +1,7 @@
+HOST=0.0.0.0
+PORT=8081
+
+NUXT_PUBLIC_API_ORIGIN=http://localhost:3000
+
+NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
+NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
diff --git a/packages/frontend-2/.eslintrc.js b/packages/frontend-2/.eslintrc.js
new file mode 100644
index 000000000..cdc4f33c5
--- /dev/null
+++ b/packages/frontend-2/.eslintrc.js
@@ -0,0 +1,129 @@
+const mainExtends = [
+ 'plugin:nuxt/recommended',
+ 'plugin:vue/vue3-recommended',
+ 'plugin:storybook/recommended',
+ 'prettier'
+]
+
+/** @type {import('eslint').Linter.Config} */
+const config = {
+ env: {
+ node: true
+ },
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module',
+ parser: '@typescript-eslint/parser',
+ tsconfigRootDir: __dirname,
+ project: ['./tsconfig.eslint.json'],
+ extraFileExtensions: ['.vue']
+ },
+ extends: [...mainExtends],
+ plugins: ['@typescript-eslint'],
+ ignorePatterns: [
+ '**/templates/*',
+ 'coverage',
+ 'lib/common/generated/**/*',
+ 'storybook-static',
+ '!.storybook',
+ '.nuxt',
+ '.output'
+ ],
+ rules: {
+ camelcase: [
+ 'error',
+ {
+ properties: 'always',
+ allow: ['^[\\w]+_[\\w]+Fragment$']
+ }
+ ],
+ 'no-alert': 'error',
+ eqeqeq: ['error', 'always', { null: 'always' }],
+ 'no-console': 'off',
+ 'no-var': 'error'
+ },
+ overrides: [
+ {
+ files: '*.test.{ts,js}',
+ env: {
+ jest: true
+ }
+ },
+ {
+ files: './{components|pages|store|lib}/*.{js,ts,vue}',
+ env: {
+ node: false,
+ browser: true
+ }
+ },
+ {
+ files: '*.{ts,tsx,vue}',
+ extends: ['plugin:@typescript-eslint/recommended', ...mainExtends],
+ rules: {
+ '@typescript-eslint/no-explicit-any': ['error'],
+ '@typescript-eslint/no-unsafe-argument': ['error'],
+ '@typescript-eslint/no-unsafe-assignment': 'error',
+ '@typescript-eslint/no-unsafe-call': 'error',
+ '@typescript-eslint/no-unsafe-member-access': 'error',
+ '@typescript-eslint/no-unsafe-return': 'error',
+ '@typescript-eslint/no-for-in-array': ['error'],
+ '@typescript-eslint/restrict-template-expressions': ['error'],
+ '@typescript-eslint/restrict-plus-operands': ['error'],
+ '@typescript-eslint/await-thenable': ['warn'],
+ '@typescript-eslint/ban-types': ['warn'],
+ 'require-await': 'off',
+ '@typescript-eslint/require-await': 'error',
+ 'no-undef': 'off'
+ }
+ },
+ {
+ files: '*.vue',
+ plugins: ['vuejs-accessibility'],
+ extends: [
+ 'plugin:@typescript-eslint/recommended',
+ ...mainExtends,
+ 'plugin:vuejs-accessibility/recommended'
+ ],
+ rules: {
+ 'vue/component-tags-order': [
+ 'error',
+ { order: ['docs', 'template', 'script', 'style'] }
+ ],
+ 'vue/require-default-prop': 'off',
+ 'vue/multi-word-component-names': 'off',
+ 'vue/component-name-in-template-casing': [
+ 'error',
+ 'PascalCase',
+ { registeredComponentsOnly: false }
+ ],
+ 'vuejs-accessibility/label-has-for': [
+ 'error',
+ {
+ required: {
+ some: ['nesting', 'id']
+ }
+ }
+ ]
+ }
+ },
+ {
+ files: '*.d.ts',
+ rules: {
+ 'no-var': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/ban-types': 'off'
+ }
+ },
+ {
+ files: '*.stories.{js,ts}',
+ rules: {
+ // this one feels busted, tells me to await synchronous calls
+ 'storybook/await-interactions': 'off',
+ // storybook types suck and can't be augmented
+ '@typescript-eslint/no-unsafe-call': 'off'
+ }
+ }
+ ]
+}
+
+module.exports = config
diff --git a/packages/frontend-2/.gitignore b/packages/frontend-2/.gitignore
new file mode 100644
index 000000000..438cb0860
--- /dev/null
+++ b/packages/frontend-2/.gitignore
@@ -0,0 +1,8 @@
+node_modules
+*.log*
+.nuxt
+.nitro
+.cache
+.output
+.env
+dist
diff --git a/packages/frontend-2/.storybook/main.ts b/packages/frontend-2/.storybook/main.ts
new file mode 100644
index 000000000..70fdab549
--- /dev/null
+++ b/packages/frontend-2/.storybook/main.ts
@@ -0,0 +1,76 @@
+import dotenv from 'dotenv'
+import Unimport from 'unimport/unplugin'
+import { flatten } from 'lodash-es'
+import type { StorybookConfig } from '@storybook/vue3-vite'
+import { mergeConfig, InlineConfig } from 'vite'
+import jiti from 'jiti'
+
+// used in nuxt.config.ts
+process.env.IS_STORYBOOK_BUILD = 'true'
+
+// make nuxt env vars available here
+dotenv.config()
+
+// having to use jiti cause of weird transpilation stuff going on during the storybook build
+const jitiImport = jiti(__filename, {
+ cache: false,
+ esmResolve: true
+})
+const nuxtViteConfigUtil = jitiImport(
+ '../lib/fake-nuxt-env/utils/nuxtViteConfig.mjs'
+) as typeof import('~~/lib/fake-nuxt-env/utils/nuxtViteConfig.mjs')
+const storyPaths = ['stories', 'components', 'pages', 'lib', 'layouts']
+const storiesPairs = storyPaths.map((p) => [
+ `../${p}/**/*.stories.mdx`,
+ `../${p}/**/*.stories.@(js|ts)`
+])
+const stories = flatten(storiesPairs)
+
+/**
+ * STORYBOOK CONFIG STARTS HERE
+ */
+const config: StorybookConfig = {
+ stories,
+ addons: [
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ '@storybook/addon-interactions',
+ '@storybook/addon-a11y'
+ ],
+ framework: {
+ name: '@storybook/vue3-vite',
+ options: {}
+ },
+ features: {
+ storyStoreV7: true
+ },
+ async viteFinal(config) {
+ const now = performance.now()
+ console.log('Integrating Nuxt into Storybook...')
+ const { resolvedViteConfig, nuxt } =
+ await nuxtViteConfigUtil.integrateNuxtIntoStorybook()
+ const { unimportOptions } = await nuxtViteConfigUtil.getNuxtModuleConfigs(nuxt)
+ console.log(`...done [${Math.ceil(performance.now() - now)}ms]`)
+ const customConfig: InlineConfig = {
+ plugins: [
+ // Auto-imports managed by unimport
+ // TODO: Is this already handled through nuxtViteConfig? Global functions seem to work without this
+ Unimport.vite(unimportOptions)
+ ],
+ define: {
+ 'process.server': false,
+ 'process.client': true
+ },
+ build: {
+ sourcemap: false
+ }
+ }
+ let final = mergeConfig(config, resolvedViteConfig)
+ final = mergeConfig(final, customConfig)
+ return final
+ },
+ docs: {
+ autodocs: true
+ }
+}
+export default config
diff --git a/packages/frontend-2/.storybook/preview-head.html b/packages/frontend-2/.storybook/preview-head.html
new file mode 100644
index 000000000..7100922b6
--- /dev/null
+++ b/packages/frontend-2/.storybook/preview-head.html
@@ -0,0 +1,3 @@
+
diff --git a/packages/frontend-2/.storybook/preview.js b/packages/frontend-2/.storybook/preview.js
new file mode 100644
index 000000000..7086010fc
--- /dev/null
+++ b/packages/frontend-2/.storybook/preview.js
@@ -0,0 +1,209 @@
+import '~~/assets/css/tailwind.css'
+import { setupVueApp } from '~~/lib/fake-nuxt-env/utils/nuxtAppBootstrapper'
+import { MockedApolloProvider } from '~~/lib/fake-nuxt-env/components/MockedApolloProvider'
+import { setup } from '@storybook/vue3'
+import SingletonManagers from '~~/components/singleton/Managers.vue'
+import { useArgs, useGlobals } from '@storybook/client-api'
+import { provide, watch } from 'vue'
+import { AppTheme, useTheme } from '~~/lib/core/composables/theme'
+
+setup((app) => {
+ setupVueApp(app)
+})
+
+export const parameters = {
+ // Main storybook params
+ viewport: {
+ viewports: {
+ mobile1: {
+ name: 'Small mobile (320px)',
+ styles: { width: '320px', height: '568px' }, // ratio 0.56
+ type: 'mobile'
+ },
+ mobile2: {
+ name: 'Large mobile (414px)',
+ styles: { width: '414px', height: '896px' }, // ratio 0.46
+ type: 'mobile'
+ },
+ SM: {
+ name: 'SM (640px)',
+ styles: { width: '640px', height: '1024px' },
+ type: 'mobile'
+ },
+ MD: {
+ name: 'MD (768px)',
+ styles: { width: '768px', height: '1024px' },
+ type: 'tablet'
+ },
+ LG: {
+ name: 'LG (1024px)',
+ styles: { width: '1024px', height: '768px' },
+ type: 'desktop'
+ },
+ XL: {
+ name: 'XL (1280px)',
+ styles: { width: '1280px', height: '768px' },
+ type: 'desktop'
+ },
+ '2XL': {
+ name: '2XL (1536px)',
+ styles: { width: '1536px', height: '1024px' },
+ type: 'desktop'
+ }
+ }
+ },
+ backgrounds: {
+ // Using tailwind theme bg values
+ default: 'foundation-page',
+ values: [
+ {
+ name: 'foundation-page',
+ value: 'var(--foundation-page)'
+ },
+ {
+ name: 'foundation',
+ value: 'var(--foundation)'
+ },
+ {
+ name: 'foundation-2',
+ value: 'var(--foundation-2)'
+ },
+ {
+ name: 'foundation-3',
+ value: 'var(--foundation-3)'
+ },
+ {
+ name: 'foundation-4',
+ value: 'var(--foundation-4)'
+ },
+ {
+ name: 'foundation-5',
+ value: 'var(--foundation-5)'
+ },
+ {
+ name: 'foundation-focus',
+ value: 'var(--foundation-focus)'
+ },
+ {
+ name: 'foundation-disabled',
+ value: 'var(--foundation-disabled)'
+ }
+ ]
+ },
+ // Custom params
+ apolloClient: {
+ MockedApolloProvider
+ }
+}
+
+/** @type {import('@storybook/csf').DecoratorFunction[]} */
+export const decorators = [
+ // Feed in updateArgs() into stories
+ (story, ctx) => {
+ const [, updateArgs] = useArgs()
+ return story({ ...ctx, updateArgs })
+ },
+
+ /**
+ * - Global CSS class setup
+ * - Theme support
+ * - Global singletons
+ */
+ (story, ctx) => {
+ const [, updateGlobals] = useGlobals()
+ const theme = ctx.globals.theme
+ const isDarkMode = theme === 'dark'
+ const {
+ parameters: { manualLayout }
+ } = ctx
+
+ if (isDarkMode) {
+ document.querySelector('html').classList.add('dark')
+ } else {
+ document.querySelector('html').classList.remove('dark')
+ }
+
+ return {
+ components: {
+ Story: story(),
+ SingletonManagers
+ },
+ setup: () => {
+ const { isDarkTheme, setTheme } = useTheme()
+
+ setTheme(isDarkMode ? AppTheme.Dark : AppTheme.Light)
+
+ watch(isDarkTheme, (isDark, oldIsDark) => {
+ if (isDark === oldIsDark) return
+
+ updateGlobals({
+ theme: isDark ? 'dark' : 'light'
+ })
+ })
+ },
+ inheritAttrs: false,
+ template: `
+
+
+ ${manualLayout ? '' : ''}
+
+ `
+ }
+ },
+ // Apollo Mocked Provider decorator
+ (story, ctx) => {
+ const {
+ parameters: {
+ apolloClient: { MockedApolloProvider, ...providerProps }
+ }
+ } = ctx
+
+ if (!MockedApolloProvider) {
+ console.error(
+ 'Apollo MockedApolloProvider missing from parameters in preview.js!'
+ )
+ return { template: ``, components: { Story: story() } }
+ }
+
+ return {
+ data: () => ({ providerProps }),
+ components: { MockedApolloProvider, Story: story() },
+ template: `
+
+ `
+ }
+ },
+ // Mocked router
+ (story, ctx) => {
+ const {
+ parameters: { vueRouter: { route } = { route: undefined } }
+ } = ctx
+
+ return {
+ components: { Story: story() },
+ setup: () => {
+ if (route) {
+ provide('_route', route)
+ }
+ },
+ template: ``
+ }
+ }
+]
+
+export const globalTypes = {
+ theme: {
+ name: 'Theme',
+ description: 'Global theme for components',
+ defaultValue: 'light',
+ toolbar: {
+ icon: 'circlehollow',
+ // Array of plain string values or MenuItem shape (see below)
+ items: ['light', 'dark'],
+ // Property that specifies if the name of the item will be displayed
+ title: 'Theme',
+ // Change title based on selected value
+ dynamicTitle: true
+ }
+ }
+}
diff --git a/packages/frontend-2/.vscode/settings.json b/packages/frontend-2/.vscode/settings.json
new file mode 100644
index 000000000..4ef7f0183
--- /dev/null
+++ b/packages/frontend-2/.vscode/settings.json
@@ -0,0 +1,13 @@
+{
+ "css.validate": false,
+ "less.validate": false,
+ "scss.validate": false,
+ "stylelint.validate": ["css", "scss", "vue", "postcss"],
+ "stylelint.enable": true,
+ "stylelint.configFile": "${workspaceFolder}/stylelint.config.js",
+ "volar.completion.preferredTagNameCase": "pascal",
+ "javascript.suggest.autoImports": true,
+ "typescript.suggest.autoImports": true,
+ "typescript.preferences.importModuleSpecifier": "non-relative",
+ "javascript.preferences.importModuleSpecifier": "non-relative"
+}
diff --git a/packages/frontend-2/Dockerfile b/packages/frontend-2/Dockerfile
new file mode 100644
index 000000000..77682fd89
--- /dev/null
+++ b/packages/frontend-2/Dockerfile
@@ -0,0 +1,47 @@
+FROM node:16.18.0-bullseye-slim as build-stage
+ARG NODE_ENV=production
+ARG SPECKLE_SERVER_VERSION=custom
+
+WORKDIR /speckle-server
+
+COPY .yarnrc.yml .
+COPY .yarn ./.yarn
+COPY package.json yarn.lock ./
+
+COPY packages/viewer/package.json ./packages/viewer/
+COPY packages/objectloader/package.json ./packages/objectloader/
+COPY packages/shared/package.json ./packages/shared/
+COPY packages/frontend-2/package.json ./packages/frontend-2/
+COPY packages/frontend-2/type-augmentations ./packages/frontend-2/
+
+COPY packages/objectloader ./packages/objectloader/
+COPY packages/viewer ./packages/viewer/
+COPY packages/shared ./packages/shared/
+COPY packages/frontend-2 ./packages/frontend-2/
+
+RUN yarn workspaces focus -A
+# hadolint ignore=DL3059
+RUN yarn workspaces foreach run build
+
+FROM node:16.18.0-bullseye-slim as production-stage
+ARG NODE_ENV=production
+ENV NODE_ENV=${NODE_ENV}
+
+ENV TINI_VERSION v0.19.0
+ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
+RUN chmod +x /tini
+ENTRYPOINT ["/tini", "--"]
+
+USER node
+
+ENV PORT=8080
+
+ENV NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
+ENV NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
+
+WORKDIR /speckle-server
+COPY --from=build-stage /speckle-server/packages/frontend-2/.output .
+
+EXPOSE ${PORT}
+
+CMD ["node", "./server/index.mjs"]
diff --git a/packages/frontend-2/app.vue b/packages/frontend-2/app.vue
new file mode 100644
index 000000000..2dd2abb7b
--- /dev/null
+++ b/packages/frontend-2/app.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/assets/css/tailwind-setup/semantic-colors.css b/packages/frontend-2/assets/css/tailwind-setup/semantic-colors.css
new file mode 100644
index 000000000..df3105062
--- /dev/null
+++ b/packages/frontend-2/assets/css/tailwind-setup/semantic-colors.css
@@ -0,0 +1,124 @@
+/* stylelint-disable custom-property-empty-line-before */
+/* stylelint-disable comment-empty-line-before */
+/*
+ Currently not possible to define different color values for the same color name just through
+ the tailwind config, so using CSS variables instead.
+
+ Palette reference: https://tailwindcss.com/docs/customizing-colors
+ */
+
+:root {
+ /* used only as the page background */
+ --foundation-page: #f1f5f9;
+ /* used as the background for any elements that sit on the page */
+ --foundation: #fcfcfc;
+ --foundation-2: #fcfcfc;
+ --foundation-3: #fcfcfc;
+ --foundation-4: #fcfcfc;
+ --foundation-5: #fcfcfc;
+ /* for hover/focus states */
+ --foundation-focus: #dbeafe;
+ /* for disabled backgrounds */
+ --foundation-disabled: #e5e5e5;
+
+ /* default foreground color */
+ --foreground: #334155;
+ /* dimmer foreground color, e.g. caption text */
+ --foreground-2: #94a3b8;
+ /* disabled foreground color */
+ --foreground-disabled: #a3a3a3;
+ /* primary color when used for text directly on top of foundation-page */
+ --foreground-primary: #3b82f6;
+ /* foreground color when put on top of a primary colored background */
+ --foreground-on-primary: #fff;
+
+ /* primary color */
+ --primary: #3b82f6;
+ /* focused primary color */
+ --primary-focus: #2563eb;
+ /* muted primary color */
+ --primary-muted: #3b82f60d;
+
+ /* outline variations */
+ --outline-1: #3b82f6;
+ --outline-2: #93c5fd;
+ --outline-3: #cbd5e1;
+
+ /* success variations */
+ --success: #34d399;
+ --success-lighter: #d1fae5;
+ --success-darker: #064e3b;
+
+ /* warning variations */
+ --warning: #fbbf24;
+ --warning-lighter: #fef3c7;
+ --warning-darker: #78350f;
+
+ /* info variations */
+ --info: #38bdf8;
+ --info-lighter: #e0f2fe;
+ --info-darker: #0c4a6e;
+
+ /* danger variations */
+ --danger: #f87171;
+ --danger-lighter: #fee2e2;
+ --danger-darker: #7f1d1d;
+}
+
+:root.dark {
+ /* used only as the page background */
+ --foundation-page: #18181b;
+ /* used as the background for any element that sits on the page */
+ --foundation: #27272a;
+ --foundation-2: #303034;
+ --foundation-3: #52525b;
+ --foundation-4: #71717a;
+ --foundation-5: #a1a1aa;
+ /* for hover/focus states */
+ --foundation-focus: #52525b;
+ /* for disabled backgrounds */
+ --foundation-disabled: #3c3c3d;
+
+ /* default foreground color */
+ --foreground: #f4f4f5;
+ /* dimmer foreground color, e.g. caption text */
+ --foreground-2: #71717a;
+ /* disabled foreground color */
+ --foreground-disabled: #5a5a5f;
+ /* primary color when used for text directly on top of foundation-page */
+ --foreground-primary: #bfdbfe;
+ /* foreground color when put on top of a primary colored background */
+ --foreground-on-primary: #fafafa;
+
+ /* primary color */
+ --primary: #3b82f6;
+ /* focused primary color */
+ --primary-focus: #60a5fa;
+ /* muted primary color */
+ --primary-muted: #71717a0d;
+
+ /* outline variations */
+ --outline-1: #a1a1aa;
+ --outline-2: #52525b;
+ --outline-3: #3f3f46;
+
+ /* success variations */
+ --success: #34d399;
+ --success-lighter: #a7f3d0;
+ --success-darker: #064e3b;
+
+ /* warning variations */
+ --warning: #facc15;
+ --warning-lighter: #fef08a;
+ --warning-darker: #78350f;
+
+ /* info variations */
+ --info: #38bdf8;
+ --info-lighter: #bae6fd;
+ --info-darker: #0c4a6e;
+
+ /* danger variations */
+ --danger: #f87171;
+ --danger-lighter: #fecaca;
+ --danger-darker: #7f1d1d;
+}
diff --git a/packages/frontend-2/assets/css/tailwind-setup/tippy.css b/packages/frontend-2/assets/css/tailwind-setup/tippy.css
new file mode 100644
index 000000000..d20f05c33
--- /dev/null
+++ b/packages/frontend-2/assets/css/tailwind-setup/tippy.css
@@ -0,0 +1,15 @@
+/**
+ * Tippy mentions theme
+ */
+.tippy-box[data-theme='mention'] {
+ background: none;
+ pointer-events: none;
+
+ .tippy-arrow {
+ display: none;
+ }
+
+ .tippy-content > * {
+ pointer-events: auto;
+ }
+}
diff --git a/packages/frontend-2/assets/css/tailwind.css b/packages/frontend-2/assets/css/tailwind.css
new file mode 100644
index 000000000..c5759bc4b
--- /dev/null
+++ b/packages/frontend-2/assets/css/tailwind.css
@@ -0,0 +1,155 @@
+/* stylelint-disable selector-id-pattern */
+@import './tailwind-setup/semantic-colors.css';
+@import './tailwind-setup/tippy.css';
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/**
+ * Don't pollute this - it's going to be bundled in all pages!
+ */
+
+@layer components {
+ /**
+ * Heading weights & line heights can differ, which is why we only set the font size
+ */
+ .h1 {
+ @apply text-5xl leading-10;
+ }
+
+ .h2 {
+ @apply text-4xl leading-10;
+ }
+
+ .h3 {
+ @apply text-3xl leading-9;
+ }
+
+ .h4 {
+ @apply text-2xl leading-8;
+ }
+
+ .h5 {
+ @apply text-xl leading-7;
+ }
+
+ .h6 {
+ @apply text-lg leading-6;
+ }
+
+ .label {
+ @apply text-sm font-medium leading-5;
+
+ &--light {
+ @apply font-normal;
+ }
+ }
+
+ .label-light {
+ @apply label label--light;
+ }
+
+ .normal {
+ @apply text-base font-normal;
+ }
+
+ .caption {
+ @apply text-xs;
+ }
+
+ .text-tiny {
+ font-size: 0.6rem;
+ line-height: 1rem;
+ }
+
+ /**
+ * Grid/Layout container that limits max width to expected sizes that we use in our designs
+ * (see Figma - Design System - Foundations - Grid & Layout)
+ */
+ .layout-container {
+ @apply mx-auto;
+
+ /* base/mobile - fluid, no max width, just padding */
+ @apply px-4;
+
+ /* sm+ - also fluid, increased padding */
+ @media (min-width: theme('screens.sm')) {
+ @apply px-8;
+ }
+
+ /* lg+ (from this point on, no padding just limited max width) */
+ @media (min-width: theme('screens.lg')) {
+ /* @apply px-0 max-w-[928px]; */
+ @apply px-6 max-w-full;
+ }
+
+ /* xl+ */
+ @media (min-width: theme('screens.xl')) {
+ @apply max-w-[1216px];
+ }
+
+ /* 2xl+ */
+ @media (min-width: theme('screens.2xl')) {
+ @apply max-w-[1312px];
+ }
+ }
+
+ /* Simple scrollbar (OSX-like) to use instead of the ugly browser one */
+ .simple-scrollbar {
+ scrollbar-width: var(--simple-scrollbar-width);
+ scrollbar-color: var(--foreground-2);
+
+ &::-webkit-scrollbar {
+ width: var(--simple-scrollbar-width);
+ height: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ border-radius: 15px;
+ background: var(--foundation-disabled);
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border-radius: 15px;
+ background: var(--foreground-2);
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background: var(--foreground-2);
+ }
+
+ &::-webkit-scrollbar-thumb:active {
+ background: rgba(90 90 90 10100%);
+ }
+ }
+}
+
+:root {
+ --simple-scrollbar-width: 4px;
+}
+
+/**
+ * Making sure page is always stretched to the bottom of the screen even if there's nothing in it
+ */
+html,
+body,
+div#__nuxt,
+div#__nuxt > div {
+ min-height: 100%;
+}
+
+html,
+body,
+div#__nuxt {
+ height: 100%;
+}
+
+body {
+ @apply font-sans;
+}
+
+[type='checkbox']:focus,
+[type='radio']:focus {
+ @apply ring-offset-foundation;
+}
diff --git a/packages/frontend-2/assets/images/auth/github_icon.svg b/packages/frontend-2/assets/images/auth/github_icon.svg
new file mode 100644
index 000000000..aa05db9c5
--- /dev/null
+++ b/packages/frontend-2/assets/images/auth/github_icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/frontend-2/assets/images/auth/google_icon.svg b/packages/frontend-2/assets/images/auth/google_icon.svg
new file mode 100644
index 000000000..7bb36f977
--- /dev/null
+++ b/packages/frontend-2/assets/images/auth/google_icon.svg
@@ -0,0 +1,45 @@
+
+
diff --git a/packages/frontend-2/assets/images/auth/google_icon_w_bg.svg b/packages/frontend-2/assets/images/auth/google_icon_w_bg.svg
new file mode 100644
index 000000000..4dd789573
--- /dev/null
+++ b/packages/frontend-2/assets/images/auth/google_icon_w_bg.svg
@@ -0,0 +1,35 @@
+
+
\ No newline at end of file
diff --git a/packages/frontend-2/assets/images/auth/ms_icon.svg b/packages/frontend-2/assets/images/auth/ms_icon.svg
new file mode 100644
index 000000000..1f7397648
--- /dev/null
+++ b/packages/frontend-2/assets/images/auth/ms_icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/frontend-2/assets/images/boxes/empty.png b/packages/frontend-2/assets/images/boxes/empty.png
new file mode 100755
index 000000000..399a9bb54
Binary files /dev/null and b/packages/frontend-2/assets/images/boxes/empty.png differ
diff --git a/packages/frontend-2/assets/images/comments_intro_320x248.webp b/packages/frontend-2/assets/images/comments_intro_320x248.webp
new file mode 100644
index 000000000..4848bffc8
Binary files /dev/null and b/packages/frontend-2/assets/images/comments_intro_320x248.webp differ
diff --git a/packages/frontend-2/assets/images/speckle_logo_big.png b/packages/frontend-2/assets/images/speckle_logo_big.png
new file mode 100644
index 000000000..c2a7002af
Binary files /dev/null and b/packages/frontend-2/assets/images/speckle_logo_big.png differ
diff --git a/packages/frontend-2/assets/images/speckle_text_logo_white.svg b/packages/frontend-2/assets/images/speckle_text_logo_white.svg
new file mode 100644
index 000000000..8806b2f46
--- /dev/null
+++ b/packages/frontend-2/assets/images/speckle_text_logo_white.svg
@@ -0,0 +1,20 @@
+
diff --git a/packages/frontend-2/codegen.ts b/packages/frontend-2/codegen.ts
new file mode 100644
index 000000000..1614f18a4
--- /dev/null
+++ b/packages/frontend-2/codegen.ts
@@ -0,0 +1,28 @@
+import type { CodegenConfig } from '@graphql-codegen/cli'
+
+const config: CodegenConfig = {
+ schema: 'http://127.0.0.1:3000/graphql',
+ documents: ['{lib,components,layouts,pages,middleware}/**/*.{vue,js,ts}'],
+ ignoreNoDocuments: true, // for better experience with the watcher
+ generates: {
+ './lib/common/generated/gql/': {
+ preset: 'client',
+ config: {
+ useTypeImports: true,
+ fragmentMasking: false,
+ dedupeFragments: true,
+ scalars: {
+ JSONObject: '{}',
+ DateTime: 'string'
+ }
+ },
+ presetConfig: {
+ fragmentMasking: false,
+ dedupeFragments: true
+ },
+ plugins: []
+ }
+ }
+}
+
+export default config
diff --git a/packages/frontend-2/components/ActiveUserTest.vue b/packages/frontend-2/components/ActiveUserTest.vue
new file mode 100644
index 000000000..10c18e218
--- /dev/null
+++ b/packages/frontend-2/components/ActiveUserTest.vue
@@ -0,0 +1,27 @@
+
+
+ Active user test:
+
+ {{ activeUser ? activeUser.id : 'none' }}
+
+
+ {{ data ? data.activeUser?.id : 'none' }}
+
+
+
+
diff --git a/packages/frontend-2/components/DemoNuxtFunctionality.stories.ts b/packages/frontend-2/components/DemoNuxtFunctionality.stories.ts
new file mode 100644
index 000000000..a5c5a1ed4
--- /dev/null
+++ b/packages/frontend-2/components/DemoNuxtFunctionality.stories.ts
@@ -0,0 +1,22 @@
+import DemoNuxtFunctionality from '~~/components/DemoNuxtFunctionality.vue'
+import { StoryObj, Meta } from '@storybook/vue3'
+
+export default {
+ title: 'Appendix/Test/Demo Nuxt Functionality (test)',
+ component: DemoNuxtFunctionality,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'This invokes various Nuxt globals (funcs & Vue components) as a test to ensure the Nuxt-Storybook integration works'
+ }
+ }
+ }
+} as Meta
+
+export const Default: StoryObj = {
+ render: () => ({
+ components: { DemoNuxtFunctionality },
+ template: ``
+ })
+}
diff --git a/packages/frontend-2/components/DemoNuxtFunctionality.vue b/packages/frontend-2/components/DemoNuxtFunctionality.vue
new file mode 100644
index 000000000..d05cfec31
--- /dev/null
+++ b/packages/frontend-2/components/DemoNuxtFunctionality.vue
@@ -0,0 +1,20 @@
+
+
+ Link to exmaple.com
+
+
+
+
diff --git a/packages/frontend-2/components/DesignSystem.stories.ts b/packages/frontend-2/components/DesignSystem.stories.ts
new file mode 100644
index 000000000..0513925c2
--- /dev/null
+++ b/packages/frontend-2/components/DesignSystem.stories.ts
@@ -0,0 +1,18 @@
+import { Meta, StoryObj } from '@storybook/vue3'
+import DesignSystem from '~~/components/DesignSystem.vue'
+
+export default {
+ title: 'Overview/Styling/Design System Examples',
+ component: DesignSystem
+} as Meta
+
+export const Default: StoryObj = {
+ render: () => ({
+ components: { DesignSystem },
+ template: `
+
+
+
+ `
+ })
+}
diff --git a/packages/frontend-2/components/DesignSystem.vue b/packages/frontend-2/components/DesignSystem.vue
new file mode 100644
index 000000000..61407cff3
--- /dev/null
+++ b/packages/frontend-2/components/DesignSystem.vue
@@ -0,0 +1,436 @@
+
+
+
+
+
Design System Demo
+
+
+
+
+
+
+ Typograhpy
+
+
+
+ Classic headings:
+
+
Heading 1
+
Heading 2
+
Heading 3
+
Heading 4
+
Heading 5
+
Normal text
+
Label text
+
Label text (light)
+
Caption text
+
+
+ Font faces?
+ system ui only - using github's stack. Lighter, better performance, no more
+ google fonts trakcing and a more native look.
+
+
+
Font weights:
+
+ - Thin
+
+ - Light
+ - Normal
+ - Medium
+ - SemiBold
+ - Bold
+
+ - Black
+
+
+
+
+
+
+ Colors
+
+
+ Here are the available foreground colors:
+
+
+ Text colors?
+
+ foreground
+ for text you want to stand out.
+
+
+ foreground-2
+ for text you don't want to stand out.
+
+
+
+
+
+ - text-foreground
+ - text-foreground-2
+
+
+
+
+
+
+ Buttons
+
+
+
+
+
+
+
+ Avatars
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Links & form elements
+
+
+
+
Link:
+
+ Basic Link
+ |
+ Disabled Link
+
+
+
+
+
+
+
+ Components
+
+ TODO
+
+
+
+
+
diff --git a/packages/frontend-2/components/auth/LoginPanel.vue b/packages/frontend-2/components/auth/LoginPanel.vue
new file mode 100644
index 000000000..bdde2af97
--- /dev/null
+++ b/packages/frontend-2/components/auth/LoginPanel.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Speckle Login
+
+
+ Interoperability, Collaboration and Automation for 3D
+
+
+
+
+
+ {{
+ hasThirdPartyStrategies
+ ? 'Or login with your email'
+ : 'Login with your email'
+ }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/auth/LoginWithEmailBlock.vue b/packages/frontend-2/components/auth/LoginWithEmailBlock.vue
new file mode 100644
index 000000000..8aa432739
--- /dev/null
+++ b/packages/frontend-2/components/auth/LoginWithEmailBlock.vue
@@ -0,0 +1,87 @@
+
+
+
+
diff --git a/packages/frontend-2/components/auth/PasswordChecks.vue b/packages/frontend-2/components/auth/PasswordChecks.vue
new file mode 100644
index 000000000..882215d06
--- /dev/null
+++ b/packages/frontend-2/components/auth/PasswordChecks.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
8+ characters long
+
+
+
+
+
+
One lowercase letter
+
+
+
+
+
One uppercase letter
+
+
+
+
+
diff --git a/packages/frontend-2/components/auth/PasswordResetFinalizationPanel.vue b/packages/frontend-2/components/auth/PasswordResetFinalizationPanel.vue
new file mode 100644
index 000000000..16fead661
--- /dev/null
+++ b/packages/frontend-2/components/auth/PasswordResetFinalizationPanel.vue
@@ -0,0 +1,57 @@
+
+
+
+
+ One step closer to resetting your password.
+
+
+
+
+
+
+
+
+
+ Save new password
+
+
+
+
diff --git a/packages/frontend-2/components/auth/PasswordResetPanel.vue b/packages/frontend-2/components/auth/PasswordResetPanel.vue
new file mode 100644
index 000000000..9d0dd0724
--- /dev/null
+++ b/packages/frontend-2/components/auth/PasswordResetPanel.vue
@@ -0,0 +1,43 @@
+
+
+
+ Reset your account password
+
+
+
+
+ Type in the email address you used, so we can verify your account. We will
+ send you instructions on how to reset your password.
+
+
+
+
+
+
+
+ Send reset e-mail
+
+
+
+
diff --git a/packages/frontend-2/components/auth/RegisterPanel.vue b/packages/frontend-2/components/auth/RegisterPanel.vue
new file mode 100644
index 000000000..b216c6538
--- /dev/null
+++ b/packages/frontend-2/components/auth/RegisterPanel.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+ Create your Speckle Account
+
+
+ Interoperability, Collaboration and Automation for 3D
+
+
+
+
+
+
+ This server is invite only. If you have received an invitation email, please
+ follow the instructions in it.
+
+
+
+ Already have an account?
+ Log in
+
+
+
+
+
+
+ Or sign up with your email
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue b/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue
new file mode 100644
index 000000000..885324cbf
--- /dev/null
+++ b/packages/frontend-2/components/auth/RegisterWithEmailBlock.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/auth/VerificationReminder.vue b/packages/frontend-2/components/auth/VerificationReminder.vue
new file mode 100644
index 000000000..3e5f4a128
--- /dev/null
+++ b/packages/frontend-2/components/auth/VerificationReminder.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
{{ verifyBannerText }}
+
+
+ {{ verifyBannerCtaText }}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/auth/VerificationReminderMenuNotice.vue b/packages/frontend-2/components/auth/VerificationReminderMenuNotice.vue
new file mode 100644
index 000000000..c3e2a402d
--- /dev/null
+++ b/packages/frontend-2/components/auth/VerificationReminderMenuNotice.vue
@@ -0,0 +1,118 @@
+
+
+
{{ verifyBannerText }}
+
+
+ {{ verifyBannerCtaText }}
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/auth/third-party/LoginBlock.vue b/packages/frontend-2/components/auth/third-party/LoginBlock.vue
new file mode 100644
index 000000000..d9500ac90
--- /dev/null
+++ b/packages/frontend-2/components/auth/third-party/LoginBlock.vue
@@ -0,0 +1,95 @@
+
+
+
+ onClick(strat)"
+ />
+
+
+
+
diff --git a/packages/frontend-2/components/auth/third-party/LoginButtonBase.vue b/packages/frontend-2/components/auth/third-party/LoginButtonBase.vue
new file mode 100644
index 000000000..78b66ed08
--- /dev/null
+++ b/packages/frontend-2/components/auth/third-party/LoginButtonBase.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/auth/third-party/LoginButtonGithub.vue b/packages/frontend-2/components/auth/third-party/LoginButtonGithub.vue
new file mode 100644
index 000000000..90f0226c7
--- /dev/null
+++ b/packages/frontend-2/components/auth/third-party/LoginButtonGithub.vue
@@ -0,0 +1,15 @@
+
+
+
+ Github
+
+
+
diff --git a/packages/frontend-2/components/auth/third-party/LoginButtonGoogle.vue b/packages/frontend-2/components/auth/third-party/LoginButtonGoogle.vue
new file mode 100644
index 000000000..7ad6f23da
--- /dev/null
+++ b/packages/frontend-2/components/auth/third-party/LoginButtonGoogle.vue
@@ -0,0 +1,15 @@
+
+
+
+ Google
+
+
+
diff --git a/packages/frontend-2/components/auth/third-party/LoginButtonMicrosoft.vue b/packages/frontend-2/components/auth/third-party/LoginButtonMicrosoft.vue
new file mode 100644
index 000000000..ace9791c5
--- /dev/null
+++ b/packages/frontend-2/components/auth/third-party/LoginButtonMicrosoft.vue
@@ -0,0 +1,11 @@
+
+
+
+ Microsoft
+
+
+
diff --git a/packages/frontend-2/components/common/Badge.stories.ts b/packages/frontend-2/components/common/Badge.stories.ts
new file mode 100644
index 000000000..fe3336fd2
--- /dev/null
+++ b/packages/frontend-2/components/common/Badge.stories.ts
@@ -0,0 +1,84 @@
+import { Meta, StoryObj } from '@storybook/vue3'
+import CommonBadge from '~~/components/common/Badge.vue'
+import { XMarkIcon } from '@heroicons/vue/20/solid'
+import { FaceSmileIcon } from '@heroicons/vue/24/outline'
+export default {
+ component: CommonBadge,
+ argTypes: {
+ default: {
+ type: 'string',
+ description: 'Default slot holds badge text'
+ },
+ iconLeft: {
+ type: 'function'
+ },
+ size: {
+ options: ['base', 'lg'],
+ control: { type: 'select' }
+ },
+ clickIcon: {
+ action: 'click',
+ type: 'function'
+ }
+ }
+} as Meta
+
+export const Default: StoryObj = {
+ render: (args) => ({
+ components: { CommonBadge },
+ setup: () => ({ args }),
+ template: `{{ args.default || 'Badge' }}`
+ }),
+ args: {
+ size: 'base',
+ dot: false,
+ rounded: false,
+ clickableIcon: false
+ }
+}
+
+export const Large: StoryObj = {
+ ...Default,
+ args: {
+ size: 'lg'
+ }
+}
+
+export const Rounded: StoryObj = {
+ ...Default,
+ args: {
+ rounded: true
+ }
+}
+
+export const WithDot: StoryObj = {
+ ...Default,
+ args: {
+ dot: true
+ }
+}
+
+export const WithIcon: StoryObj = {
+ ...Default,
+ args: {
+ iconLeft: FaceSmileIcon
+ }
+}
+
+export const WithClickableIcon: StoryObj = {
+ ...Default,
+ args: {
+ clickableIcon: true,
+ iconLeft: XMarkIcon
+ }
+}
+
+export const WithCustomColors: StoryObj = {
+ ...Default,
+ args: {
+ iconLeft: FaceSmileIcon,
+ dot: true,
+ colorClasses: 'text-info bg-warning',
+ dotIconColorClasses: 'text-success'
+ }
+}
diff --git a/packages/frontend-2/components/common/Badge.vue b/packages/frontend-2/components/common/Badge.vue
new file mode 100644
index 000000000..781ff8659
--- /dev/null
+++ b/packages/frontend-2/components/common/Badge.vue
@@ -0,0 +1,115 @@
+
+
+
+ Badge
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/common/EmptySearchState.vue b/packages/frontend-2/components/common/EmptySearchState.vue
new file mode 100644
index 000000000..3348c68c1
--- /dev/null
+++ b/packages/frontend-2/components/common/EmptySearchState.vue
@@ -0,0 +1,16 @@
+
+
+

+
+ No items matching your search query found!
+
+
$emit('clear-search')">Clear Search
+
+
+
diff --git a/packages/frontend-2/components/common/loading/Bar.vue b/packages/frontend-2/components/common/loading/Bar.vue
new file mode 100644
index 000000000..f691a31d3
--- /dev/null
+++ b/packages/frontend-2/components/common/loading/Bar.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
diff --git a/packages/frontend-2/components/common/model/Select.vue b/packages/frontend-2/components/common/model/Select.vue
new file mode 100644
index 000000000..9ec30c6ef
--- /dev/null
+++ b/packages/frontend-2/components/common/model/Select.vue
@@ -0,0 +1,146 @@
+
+
+
+
+ {{ multiple ? 'Select models' : 'Select a model' }}
+
+
+
+
+
+
+
+ {{ branch.name }}
+
+
+
+ +{{ hiddenSelectedItemCount }}
+
+
+
+
+
+
+ {{ (isArrayValue(value) ? value[0] : value).name }}
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/common/steps/Bullet.stories.ts b/packages/frontend-2/components/common/steps/Bullet.stories.ts
new file mode 100644
index 000000000..225464c8c
--- /dev/null
+++ b/packages/frontend-2/components/common/steps/Bullet.stories.ts
@@ -0,0 +1,107 @@
+import { action } from '@storybook/addon-actions'
+import { Meta, StoryObj } from '@storybook/vue3'
+import CommonStepsBullet from '~~/components/common/steps/Bullet.vue'
+import { BulletStepType } from '~~/lib/common/helpers/components'
+import { mergeStories } from '~~/lib/common/helpers/storybook'
+import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
+
+type StoryType = StoryObj<
+ Record & {
+ 'update:modelValue': (val: boolean) => void
+ }
+>
+
+const testSteps: BulletStepType[] = [
+ {
+ name: 'First step',
+ onClick: action('step-clicked')
+ },
+ {
+ name: 'Second step',
+ onClick: action('step-clicked')
+ },
+ {
+ name: 'Third step',
+ onClick: action('step-clicked')
+ }
+]
+
+export default {
+ component: CommonStepsBullet,
+ argTypes: {
+ orientation: {
+ options: ['horizontal', 'vertical'],
+ control: { type: 'select' }
+ },
+ 'update:modelValue': {
+ type: 'function',
+ action: 'v-model'
+ }
+ },
+ parameters: {
+ docs: {
+ description: {
+ component: 'Bullet-based steps component'
+ }
+ }
+ }
+} as Meta
+
+export const Default: StoryType = {
+ render: (args, ctx) => ({
+ components: { CommonStepsBullet },
+ setup: () => ({ args }),
+ template: ``,
+ methods: {
+ onModelUpdate(val: boolean) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ }),
+ args: {
+ ariaLabel: 'Steps ARIA title!',
+ basic: false,
+ orientation: 'horizontal',
+ steps: testSteps,
+ modelValue: 1,
+ nonInteractive: false
+ }
+}
+
+export const Vertical: StoryType = mergeStories(Default, {
+ args: {
+ orientation: 'vertical'
+ }
+})
+
+export const VersionBasic: StoryType = mergeStories(Default, {
+ args: {
+ basic: true
+ }
+})
+
+export const StartOnNegativeStep: StoryType = mergeStories(Default, {
+ args: {
+ modelValue: -1
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Start on -1 step (on neither of the steps)'
+ }
+ }
+ }
+})
+
+export const GoVerticalBelowBreakpoint: StoryType = mergeStories(Default, {
+ args: {
+ goVerticalBelow: TailwindBreakpoints.md
+ }
+})
+
+export const NonInteractive: StoryType = mergeStories(Default, {
+ args: {
+ nonInteractive: true
+ }
+})
diff --git a/packages/frontend-2/components/common/steps/Bullet.vue b/packages/frontend-2/components/common/steps/Bullet.vue
new file mode 100644
index 000000000..a58646cf5
--- /dev/null
+++ b/packages/frontend-2/components/common/steps/Bullet.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
diff --git a/packages/frontend-2/components/common/steps/Number.stories.ts b/packages/frontend-2/components/common/steps/Number.stories.ts
new file mode 100644
index 000000000..7ef9de6e9
--- /dev/null
+++ b/packages/frontend-2/components/common/steps/Number.stories.ts
@@ -0,0 +1,119 @@
+import { action } from '@storybook/addon-actions'
+import { Meta, StoryObj } from '@storybook/vue3'
+import CommonStepsNumber from '~~/components/common/steps/Number.vue'
+import { NumberStepType } from '~~/lib/common/helpers/components'
+import { mergeStories } from '~~/lib/common/helpers/storybook'
+import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
+
+type StoryType = StoryObj<
+ Record & {
+ 'update:modelValue': (val: boolean) => void
+ }
+>
+
+const testStepsWithDescription: NumberStepType[] = [
+ {
+ name: 'First step',
+ onClick: action('step-clicked'),
+ description: 'Some example text'
+ },
+ {
+ name: 'Second step',
+ onClick: action('step-clicked'),
+ description: 'More example text'
+ },
+ {
+ name: 'Third step',
+ onClick: action('step-clicked'),
+ description: 'Final example text'
+ }
+]
+
+const testStepsWithoutDescription: NumberStepType[] = [
+ {
+ name: 'First step',
+ onClick: action('step-clicked')
+ },
+ {
+ name: 'Second step',
+ onClick: action('step-clicked')
+ },
+ {
+ name: 'Third step',
+ onClick: action('step-clicked')
+ }
+]
+
+export default {
+ component: CommonStepsNumber,
+ argTypes: {
+ orientation: {
+ options: ['horizontal', 'vertical'],
+ control: { type: 'select' }
+ },
+ 'update:modelValue': {
+ type: 'function',
+ action: 'v-model'
+ }
+ },
+ parameters: {
+ docs: {
+ description: {
+ component: 'Number-based steps component. Also supports optional description.'
+ }
+ }
+ }
+} as Meta
+
+export const Default: StoryType = {
+ render: (args, ctx) => ({
+ components: { CommonStepsNumber },
+ setup: () => ({ args }),
+ template: ``,
+ methods: {
+ onModelUpdate(val: boolean) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ }),
+ args: {
+ ariaLabel: 'Steps ARIA title!',
+ orientation: 'horizontal',
+ steps: testStepsWithDescription,
+ modelValue: 1
+ }
+}
+
+export const Vertical: StoryType = mergeStories(Default, {
+ args: {
+ orientation: 'vertical'
+ }
+})
+
+export const StartOnNegativeStep: StoryType = mergeStories(Default, {
+ args: {
+ modelValue: -1
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Start on -1 step (on neither of the steps)'
+ }
+ }
+ }
+})
+
+export const NoDescription: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ steps: testStepsWithoutDescription
+ }
+}
+
+export const GoVerticalBelowBreakpoint: StoryType = mergeStories(Default, {
+ args: {
+ goVerticalBelow: TailwindBreakpoints.md
+ }
+})
diff --git a/packages/frontend-2/components/common/steps/Number.vue b/packages/frontend-2/components/common/steps/Number.vue
new file mode 100644
index 000000000..d565e739d
--- /dev/null
+++ b/packages/frontend-2/components/common/steps/Number.vue
@@ -0,0 +1,106 @@
+
+
+
+
diff --git a/packages/frontend-2/components/common/text/Link.stories.ts b/packages/frontend-2/components/common/text/Link.stories.ts
new file mode 100644
index 000000000..fed364095
--- /dev/null
+++ b/packages/frontend-2/components/common/text/Link.stories.ts
@@ -0,0 +1,139 @@
+import { userEvent, within } from '@storybook/testing-library'
+import { Meta, StoryObj } from '@storybook/vue3'
+import { wait } from '@speckle/shared'
+import CommonTextLink from '~~/components/common/text/Link.vue'
+import { mergeStories, VuePlayFunction } from '~~/lib/common/helpers/storybook'
+
+export default {
+ component: CommonTextLink,
+ parameters: {
+ docs: {
+ description: {
+ component: 'Basically just a wrapper over FormButton w/ type link'
+ }
+ }
+ },
+ argTypes: {
+ to: {
+ type: 'string'
+ },
+ default: {
+ type: 'string',
+ description: 'Default slot holds button contents'
+ },
+ click: {
+ action: 'click',
+ type: 'function'
+ },
+ external: {
+ type: 'boolean'
+ },
+ disabled: {
+ type: 'boolean'
+ },
+ size: {
+ options: ['xs', 'sm', 'base', 'lg', 'xl'],
+ control: { type: 'select' }
+ }
+ }
+} as Meta
+
+const clickPlayBuilder: (rightClick: boolean) => VuePlayFunction =
+ (rightClick) =>
+ async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ const button = canvas.getByRole('link')
+
+ userEvent.click(button, rightClick ? { button: 2 } : undefined)
+
+ await wait(1000)
+
+ userEvent.tab()
+ }
+const rightClickPlay = clickPlayBuilder(true)
+const leftClickPlay = clickPlayBuilder(false)
+
+export const Default: StoryObj = {
+ render: (args) => ({
+ components: { CommonTextLink },
+ setup() {
+ return { args }
+ },
+ template: `{{ args.default || 'Link' }}`
+ }),
+ play: rightClickPlay,
+ args: {
+ to: 'https://google.com',
+ disabled: false,
+ default: 'Click me!',
+ size: 'base'
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: 'Hello World!'
+ }
+ }
+ }
+}
+
+export const Small: StoryObj = mergeStories(Default, {
+ args: {
+ size: 'sm'
+ }
+})
+
+export const ExtraSmall: StoryObj = mergeStories(Default, {
+ args: {
+ size: 'xs'
+ }
+})
+
+export const Large: StoryObj = mergeStories(Default, {
+ args: {
+ size: 'lg'
+ }
+})
+
+export const ExtraLarge: StoryObj = mergeStories(Default, {
+ args: {
+ size: 'xl'
+ }
+})
+
+export const Disabled: StoryObj = mergeStories(Default, {
+ play: leftClickPlay,
+ args: {
+ disabled: true
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Button is disabled and no mouse events fire'
+ }
+ }
+ }
+})
+
+export const NoTarget: StoryObj = mergeStories(Default, {
+ play: leftClickPlay,
+ args: {
+ to: null,
+ default: 'No URL, only for tracking click events'
+ }
+})
+
+export const External: StoryObj = mergeStories(Default, {
+ args: {
+ external: true,
+ to: '/',
+ default: 'External link'
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Forces target to be treated as an external link'
+ }
+ }
+ }
+})
diff --git a/packages/frontend-2/components/common/text/Link.vue b/packages/frontend-2/components/common/text/Link.vue
new file mode 100644
index 000000000..cae5785aa
--- /dev/null
+++ b/packages/frontend-2/components/common/text/Link.vue
@@ -0,0 +1,82 @@
+
+
+ Link
+
+
+
diff --git a/packages/frontend-2/components/common/tiptap/EmailMentionPopup.vue b/packages/frontend-2/components/common/tiptap/EmailMentionPopup.vue
new file mode 100644
index 000000000..2217f8690
--- /dev/null
+++ b/packages/frontend-2/components/common/tiptap/EmailMentionPopup.vue
@@ -0,0 +1,55 @@
+
+
+
+ Invite {{ query }}
+
+
+
diff --git a/packages/frontend-2/components/common/tiptap/MentionList.vue b/packages/frontend-2/components/common/tiptap/MentionList.vue
new file mode 100644
index 000000000..9fb4029ab
--- /dev/null
+++ b/packages/frontend-2/components/common/tiptap/MentionList.vue
@@ -0,0 +1,88 @@
+
+
+
+
+ -
+ selectItem(i)"
+ />
+
+
+
+ - Couldn't find anything 🤷
+
+
+
+
+
diff --git a/packages/frontend-2/components/common/tiptap/MentionListItem.vue b/packages/frontend-2/components/common/tiptap/MentionListItem.vue
new file mode 100644
index 000000000..6287d5d9d
--- /dev/null
+++ b/packages/frontend-2/components/common/tiptap/MentionListItem.vue
@@ -0,0 +1,27 @@
+
+
+ $emit('click', $event)"
+ >
+ {{ item.name }}
+
+ {{ item.company }}
+
+
+
+
diff --git a/packages/frontend-2/components/common/tiptap/TextEditor.vue b/packages/frontend-2/components/common/tiptap/TextEditor.vue
new file mode 100644
index 000000000..8405ba711
--- /dev/null
+++ b/packages/frontend-2/components/common/tiptap/TextEditor.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
diff --git a/packages/frontend-2/components/connectors/Card.vue b/packages/frontend-2/components/connectors/Card.vue
new file mode 100644
index 000000000..2c4efa686
--- /dev/null
+++ b/packages/frontend-2/components/connectors/Card.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+ updated {{ lastUpdated }}
+
+
+
+
+
+
{{ tag.name }}
+
+ {{ tag.stable ? tag.stable : tag.versions[0].Number }}
+
+
+
+
+
![featured image]()
+
+
+
+
+
+ Downloads
+
+
+ Tutorials
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/connectors/VersionsDownloadDialog.vue b/packages/frontend-2/components/connectors/VersionsDownloadDialog.vue
new file mode 100644
index 000000000..704f28eff
--- /dev/null
+++ b/packages/frontend-2/components/connectors/VersionsDownloadDialog.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
![featured image]()
+
{{ tag.name }}
+
+
+ {{ tag.description }}
+
+
+
+ Download Latest Stable ({{ latestStableVersions.win.Number }}) Windows
+
+
+ Download Latest Stable ({{ latestStableVersions.mac.Number }}) Mac OS
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/error/page/ProjectInviteBanner.vue b/packages/frontend-2/components/error/page/ProjectInviteBanner.vue
new file mode 100644
index 000000000..c60089765
--- /dev/null
+++ b/packages/frontend-2/components/error/page/ProjectInviteBanner.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/Button.stories.ts b/packages/frontend-2/components/form/Button.stories.ts
new file mode 100644
index 000000000..9bfb9587d
--- /dev/null
+++ b/packages/frontend-2/components/form/Button.stories.ts
@@ -0,0 +1,265 @@
+import { userEvent, within } from '@storybook/testing-library'
+import FormButton from '~~/components/form/Button.vue'
+import { StoryObj, Meta } from '@storybook/vue3'
+import { wait } from '@speckle/shared'
+import { VuePlayFunction, mergeStories } from '~~/lib/common/helpers/storybook'
+import { XMarkIcon } from '@heroicons/vue/24/solid'
+
+export default {
+ component: FormButton,
+ argTypes: {
+ color: {
+ options: ['default', 'invert', 'danger', 'warning', 'secondary'],
+ control: { type: 'select' }
+ },
+ outlined: {
+ type: 'boolean'
+ },
+ rounded: {
+ type: 'boolean'
+ },
+ to: {
+ type: 'string'
+ },
+ default: {
+ type: 'string',
+ description: 'Default slot holds button contents'
+ },
+ click: {
+ action: 'click',
+ type: 'function'
+ },
+ size: {
+ options: ['xs', 'sm', 'base', 'lg', 'xl'],
+ control: { type: 'select' }
+ },
+ fullWidth: {
+ type: 'boolean'
+ },
+ external: {
+ type: 'boolean'
+ },
+ disabled: {
+ type: 'boolean'
+ },
+ submit: {
+ type: 'boolean'
+ },
+ iconLeft: {
+ type: 'function'
+ },
+ iconRight: {
+ type: 'function'
+ }
+ },
+ parameters: {
+ docs: {
+ description: {
+ component: 'A standard button to be used anywhere you need any kind of button'
+ }
+ }
+ }
+} as Meta
+
+const clickPlayBuilder: (rightClick: boolean) => VuePlayFunction =
+ (rightClick) =>
+ async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+ const button = canvas.getByRole('button')
+
+ userEvent.click(button, rightClick ? { button: 2 } : undefined)
+
+ await wait(1000)
+
+ userEvent.tab()
+ }
+const rightClickPlay = clickPlayBuilder(true)
+const leftClickPlay = clickPlayBuilder(false)
+
+export const Default: StoryObj = {
+ render: (args) => ({
+ components: { FormButton },
+ setup() {
+ return { args }
+ },
+ template: `{{ args.default || 'Submit' }}`
+ }),
+ play: rightClickPlay,
+ args: {
+ target: '_blank',
+ to: 'https://google.com',
+ default: 'Button text',
+ size: 'base',
+ type: 'standard',
+ fullWidth: false,
+ outlined: false,
+ rounded: false,
+ text: false,
+ link: false,
+ color: 'default',
+ disabled: false,
+ submit: false,
+ hideText: false
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: 'Hello World!'
+ }
+ }
+ }
+}
+
+export const Rounded: StoryObj = mergeStories(Default, {
+ args: {
+ rounded: true
+ }
+})
+
+export const WarningButton: StoryObj = mergeStories(Default, {
+ args: {
+ color: 'warning'
+ }
+})
+
+export const RoundedOutlined: StoryObj = mergeStories(Default, {
+ args: {
+ rounded: true,
+ outlined: true
+ }
+})
+
+export const Outline: StoryObj = mergeStories(Default, {
+ args: {
+ outlined: true
+ }
+})
+
+export const Link: StoryObj = mergeStories(Default, {
+ args: {
+ link: true
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Basically just a link (CommonTextLink is an alias for this)'
+ }
+ }
+ }
+})
+
+export const Text: StoryObj = mergeStories(Default, {
+ args: {
+ text: true
+ }
+})
+
+export const Small: StoryObj = mergeStories(Default, {
+ args: {
+ size: 'sm'
+ }
+})
+
+export const ExtraSmall: StoryObj = mergeStories(Default, {
+ args: {
+ size: 'xs'
+ }
+})
+
+export const Large: StoryObj = mergeStories(Default, {
+ args: {
+ size: 'lg'
+ }
+})
+
+export const ExtraLarge: StoryObj = mergeStories(Default, {
+ args: {
+ size: 'xl'
+ }
+})
+
+export const Disabled: StoryObj = mergeStories(Default, {
+ play: leftClickPlay,
+ args: {
+ disabled: true
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Button is disabled and no mouse events fire'
+ }
+ }
+ }
+})
+
+export const NoTarget: StoryObj = mergeStories(Default, {
+ play: leftClickPlay,
+ args: {
+ to: null,
+ default: 'No URL, only for tracking click events'
+ }
+})
+
+export const FullWidth: StoryObj = mergeStories(Default, {
+ args: {
+ fullWidth: true,
+ default: 'Full width button'
+ }
+})
+
+export const External: StoryObj = mergeStories(Default, {
+ args: {
+ external: true,
+ to: '/',
+ default: 'External link'
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Forces target to be treated as an external link'
+ }
+ }
+ }
+})
+
+export const Submit: StoryObj = mergeStories(Default, {
+ play: leftClickPlay,
+ args: {
+ to: null,
+ submit: true,
+ default: 'Submit button'
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Rendered as button w/ type=submit, which will submit any parent forms'
+ }
+ }
+ }
+})
+
+export const LeftIcon: StoryObj = mergeStories(Default, {
+ args: {
+ iconLeft: XMarkIcon
+ }
+})
+
+export const RightIcon: StoryObj = mergeStories(Default, {
+ args: {
+ iconRight: XMarkIcon
+ }
+})
+
+export const IconOnBothSides: StoryObj = mergeStories(Default, {
+ args: {
+ iconRight: XMarkIcon,
+ iconLeft: XMarkIcon
+ }
+})
+
+export const IconOnly: StoryObj = mergeStories(Default, {
+ args: {
+ iconLeft: XMarkIcon,
+ hideText: true
+ }
+})
diff --git a/packages/frontend-2/components/form/Button.vue b/packages/frontend-2/components/form/Button.vue
new file mode 100644
index 000000000..87859e2ae
--- /dev/null
+++ b/packages/frontend-2/components/form/Button.vue
@@ -0,0 +1,441 @@
+
+
+
+ Button
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/CardButton.stories.ts b/packages/frontend-2/components/form/CardButton.stories.ts
new file mode 100644
index 000000000..adf9198f1
--- /dev/null
+++ b/packages/frontend-2/components/form/CardButton.stories.ts
@@ -0,0 +1,87 @@
+import { userEvent, within } from '@storybook/testing-library'
+import { Meta, StoryObj } from '@storybook/vue3'
+import { wait } from '@speckle/shared'
+import FormCardButton from '~~/components/form/CardButton.vue'
+import { VuePlayFunction } from '~~/lib/common/helpers/storybook'
+
+type StoryType = StoryObj<
+ Record & {
+ 'update:modelValue': (val: boolean) => void
+ }
+>
+
+export default {
+ component: FormCardButton,
+ argTypes: {
+ default: {
+ type: 'string',
+ description: 'Default slot holds button contents'
+ },
+ click: {
+ action: 'click',
+ type: 'function'
+ },
+ 'update:modelValue': {
+ action: 'update:modelValue',
+ type: 'function'
+ }
+ },
+ parameters: {
+ docs: {
+ description: {
+ component: 'A card button that supports a toggled/selected state'
+ }
+ }
+ }
+} as Meta
+
+const clickPlayBuilder: (rightClick?: boolean) => VuePlayFunction =
+ (rightClick) =>
+ async ({ canvasElement }) => {
+ const canvas = within(canvasElement)
+
+ userEvent.click(canvas.getByRole('button'), rightClick ? { button: 2 } : undefined)
+
+ await wait(500)
+
+ userEvent.click(canvas.getByRole('button'), rightClick ? { button: 2 } : undefined)
+
+ userEvent.tab()
+ }
+
+export const Default: StoryType = {
+ render: (args, ctx) => ({
+ components: { FormCardButton },
+ setup() {
+ return { args }
+ },
+ template: `{{ args.default || 'Text' }}`,
+ methods: {
+ onModelUpdate(val: boolean) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ }),
+ play: clickPlayBuilder(),
+ args: {
+ default: 'Architecture',
+ disabled: false,
+ modelValue: false
+ }
+}
+
+export const Disabled: StoryType = {
+ ...Default,
+ args: {
+ disabled: true
+ }
+}
+
+export const Selected: StoryType = {
+ ...Default,
+ play: clickPlayBuilder(true),
+ args: {
+ modelValue: true
+ }
+}
diff --git a/packages/frontend-2/components/form/CardButton.vue b/packages/frontend-2/components/form/CardButton.vue
new file mode 100644
index 000000000..a1f278380
--- /dev/null
+++ b/packages/frontend-2/components/form/CardButton.vue
@@ -0,0 +1,48 @@
+
+
+
+
diff --git a/packages/frontend-2/components/form/Checkbox.stories.ts b/packages/frontend-2/components/form/Checkbox.stories.ts
new file mode 100644
index 000000000..291d945f6
--- /dev/null
+++ b/packages/frontend-2/components/form/Checkbox.stories.ts
@@ -0,0 +1,176 @@
+import FormCheckbox from '~~/components/form/Checkbox.vue'
+import FormButton from '~~/components/form/Button.vue'
+import { Meta, StoryObj } from '@storybook/vue3'
+import { action } from '@storybook/addon-actions'
+import { Form, SubmissionHandler } from 'vee-validate'
+import { VuePlayFunction } from '~~/lib/common/helpers/storybook'
+import { userEvent, within } from '@storybook/testing-library'
+import { wait } from '@speckle/shared'
+import { expect } from '@storybook/jest'
+
+export default {
+ component: FormCheckbox,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'A checkbox, integrated with vee-validate for validation. Feed in rules through the `rules` prop. A checkbox can exist on its own or as part of a group. Checkboxes are grouped if they have the same name and have a parent form. The value structure differs between grouped and ungrouped checkboxes. If a checkbox is grouped, its v-model value will be an array of all values of all checked checkboxes in the group. Otherwise, its v-model value will either be its value if its checked or undefined if it isnt.'
+ }
+ }
+ },
+ argTypes: {
+ value: {
+ type: 'string'
+ },
+ rules: {
+ type: 'function'
+ },
+ 'update:modelValue': {
+ type: 'function',
+ action: 'v-model'
+ },
+ id: {
+ type: 'string'
+ }
+ }
+} as Meta
+
+const toggleCheckboxPlayFunction: VuePlayFunction = async (params) => {
+ const canvas = within(params.canvasElement)
+ const checkbox = canvas.getByRole('checkbox')
+
+ userEvent.click(checkbox)
+ await wait(1000)
+ userEvent.click(checkbox)
+}
+
+export const Default: StoryObj = {
+ play: toggleCheckboxPlayFunction,
+ render: (args) => ({
+ components: { FormCheckbox },
+ setup() {
+ const vModelAction = action('v-model')
+
+ return { args, vModelAction }
+ },
+ template: ``
+ }),
+ parameters: {
+ docs: {
+ source: {
+ code: ``
+ }
+ }
+ },
+ args: {
+ name: 'test1',
+ label: 'Example Checkbox',
+ description: 'Some help description here',
+ showRequired: false,
+ validateOnMount: false,
+ inlineDescription: false,
+ value: 'test1',
+ disabled: false
+ }
+}
+
+export const Group: StoryObj = {
+ render: (args) => ({
+ components: { FormCheckbox, Form, FormButton },
+ setup() {
+ const fooModelValueHandler = action('foo@update:modelValue')
+ const barModelValueHandler = action('bar@update:modelValue')
+ const onSubmit: SubmissionHandler = (values) => action('onSubmit')(values)
+
+ return { fooModelValueHandler, barModelValueHandler, onSubmit, args }
+ },
+ template: `
+
+ `
+ }),
+ play: async (params) => {
+ const smallDelay = 500
+ const bigDelay = 1000
+
+ const canvas = within(params.canvasElement)
+
+ const fooCheckbox = canvas.getByAltText('foo')
+ const barCheckbox = canvas.getByAltText('bar')
+ const button = canvas.getByRole('button')
+
+ userEvent.click(fooCheckbox)
+ await wait(smallDelay)
+ userEvent.click(button)
+
+ await wait(bigDelay)
+
+ userEvent.click(barCheckbox)
+ await wait(smallDelay)
+ userEvent.click(button)
+
+ await wait(bigDelay)
+
+ userEvent.click(fooCheckbox)
+ await wait(smallDelay)
+ userEvent.click(barCheckbox)
+ await wait(smallDelay)
+ userEvent.click(button)
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Checkboxes with the same name are part of a group and on form submit the value will be an array of all selected values. Check actions of this story for an example!'
+ }
+ }
+ }
+}
+
+export const InlineDescription: StoryObj = {
+ ...Default,
+ args: {
+ name: 'inline1',
+ value: 'inline1-value',
+ inlineDescription: true,
+ label: 'Example checkbox',
+ description: 'This is an inline description'
+ }
+}
+
+export const Disabled: StoryObj = {
+ ...Default,
+ play: (params) => {
+ const canvas = within(params.canvasElement)
+ const checkbox = canvas.getByRole('checkbox')
+
+ const isChecked = (checkbox as HTMLInputElement).checked
+
+ // click and assert that checkbox checked state hasn't changed
+ userEvent.click(checkbox)
+
+ const newIsChecked = (checkbox as HTMLInputElement).checked
+ expect(isChecked).toBe(newIsChecked)
+ },
+ args: {
+ name: 'disabled1',
+ value: 'disabled1-value',
+ label: 'Disabled checkbox',
+ disabled: true
+ }
+}
+
+export const Required: StoryObj = {
+ ...Default,
+ args: {
+ name: 'required1',
+ value: 'required1-value',
+ label: 'Required checkbox',
+ showRequired: true,
+ rules: [(val: string | string[]) => (val ? true : 'This field is required')],
+ validateOnMount: true
+ }
+}
diff --git a/packages/frontend-2/components/form/Checkbox.vue b/packages/frontend-2/components/form/Checkbox.vue
new file mode 100644
index 000000000..995ab19d2
--- /dev/null
+++ b/packages/frontend-2/components/form/Checkbox.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+ {{ descriptionText }}
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/TextArea.stories.ts b/packages/frontend-2/components/form/TextArea.stories.ts
new file mode 100644
index 000000000..9503dee53
--- /dev/null
+++ b/packages/frontend-2/components/form/TextArea.stories.ts
@@ -0,0 +1,115 @@
+import { userEvent, within } from '@storybook/testing-library'
+import FormTextArea from '~~/components/form/TextArea.vue'
+import { StoryObj, Meta } from '@storybook/vue3'
+import { VuePlayFunction, mergeStories } from '~~/lib/common/helpers/storybook'
+import { wait } from '@speckle/shared'
+
+export default {
+ component: FormTextArea,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'A textarea box, integrated with vee-validate for validation. Feed in rules through the `rules` prop.'
+ }
+ }
+ },
+ argTypes: {
+ rules: {
+ type: 'function'
+ },
+ 'update:modelValue': {
+ type: 'function',
+ action: 'v-model'
+ }
+ }
+} as Meta
+
+const generateRandomName = (prefix: string) => `${prefix}-${Math.random() * 100000}`
+
+const buildTextWriterPlayFunction =
+ (text: string): VuePlayFunction =>
+ async (params) => {
+ const { canvasElement, viewMode } = params
+ const canvas = within(canvasElement)
+ const input = canvas.getByRole('textbox')
+
+ // https://github.com/storybookjs/storybook/pull/19659
+ await userEvent.type(input, text, { delay: viewMode === 'story' ? 100 : 0 })
+
+ userEvent.tab()
+ }
+
+export const Default: StoryObj = {
+ render: (args) => ({
+ components: { FormTextArea },
+ setup() {
+ return { args }
+ },
+ template: `
+
+
`
+ }),
+ play: buildTextWriterPlayFunction('Hello world!'),
+ args: {
+ modelValue: '',
+ name: generateRandomName('default'),
+ help: 'Some help text',
+ placeholder: 'Placeholder text!',
+ label: 'Textarea w/ Label:',
+ showRequired: false,
+ showLabel: true,
+ disabled: false,
+ validateOnMount: false
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: ``
+ }
+ }
+ }
+}
+
+export const Required = mergeStories(Default, {
+ play: async (params) => {
+ const { canvasElement } = params
+ await buildTextWriterPlayFunction('some text')(params)
+
+ await wait(1000)
+
+ const canvas = within(canvasElement)
+ const input = canvas.getByRole('textbox')
+
+ userEvent.clear(input)
+
+ await wait(1000)
+
+ userEvent.tab()
+ },
+ args: {
+ name: generateRandomName('required'),
+ label: 'This one is required!',
+ showRequired: true,
+ rules: [(val: string) => (val ? true : 'This field is required')],
+ validateOnMount: true
+ }
+})
+
+export const Disabled = mergeStories(Default, {
+ play: buildTextWriterPlayFunction('12345'),
+ args: {
+ name: generateRandomName('disabled'),
+ label: 'Disabled input',
+ disabled: true
+ }
+})
+
+export const WithClear = mergeStories(Default, {
+ play: buildTextWriterPlayFunction('12345'),
+ args: {
+ name: generateRandomName('withclear'),
+ label: 'Click on cross to clear',
+ showClear: true
+ }
+})
diff --git a/packages/frontend-2/components/form/TextArea.vue b/packages/frontend-2/components/form/TextArea.vue
new file mode 100644
index 000000000..030ebd258
--- /dev/null
+++ b/packages/frontend-2/components/form/TextArea.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+ {{ helpTip }}
+
+
+
+
diff --git a/packages/frontend-2/components/form/TextInput.stories.ts b/packages/frontend-2/components/form/TextInput.stories.ts
new file mode 100644
index 000000000..b5f2bb752
--- /dev/null
+++ b/packages/frontend-2/components/form/TextInput.stories.ts
@@ -0,0 +1,164 @@
+import { userEvent, within } from '@storybook/testing-library'
+import FormTextInput from '~~/components/form/TextInput.vue'
+import FormButton from '~~/components/form/Button.vue'
+import { StoryObj, Meta } from '@storybook/vue3'
+import { VuePlayFunction, mergeStories } from '~~/lib/common/helpers/storybook'
+import { wait } from '@speckle/shared'
+
+export default {
+ component: FormTextInput,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'A text input box, integrated with vee-validate for validation. Feed in rules through the `rules` prop.'
+ }
+ }
+ },
+ argTypes: {
+ type: {
+ options: ['text', 'email', 'password', 'url', 'search'],
+ control: { type: 'select' }
+ },
+ rules: {
+ type: 'function'
+ },
+ 'update:modelValue': {
+ type: 'function',
+ action: 'v-model'
+ }
+ }
+} as Meta
+
+const generateRandomName = (prefix: string) => `${prefix}-${Math.random() * 100000}`
+
+const buildTextWriterPlayFunction =
+ (text: string): VuePlayFunction =>
+ async (params) => {
+ const { canvasElement, viewMode } = params
+ const canvas = within(canvasElement)
+ const input = canvas.getByRole('textbox')
+
+ // https://github.com/storybookjs/storybook/pull/19659
+ await userEvent.type(input, text, { delay: viewMode === 'story' ? 100 : 0 })
+
+ userEvent.tab()
+ }
+
+export const Default: StoryObj = {
+ render: (args) => ({
+ components: { FormTextInput },
+ setup() {
+ return { args }
+ },
+ template: `
+
+
`
+ }),
+ play: buildTextWriterPlayFunction('Hello world!'),
+ args: {
+ modelValue: '',
+ type: 'text',
+ name: generateRandomName('default'),
+ help: 'Some help text',
+ placeholder: 'Placeholder text!',
+ label: 'Text input w/ Label:',
+ showRequired: false,
+ showLabel: true,
+ disabled: false,
+ validateOnMount: false,
+ inputClasses: ''
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: ``
+ }
+ }
+ }
+}
+
+export const Email: StoryObj = mergeStories(Default, {
+ play: buildTextWriterPlayFunction('admin@example.com'),
+ args: {
+ type: 'email',
+ name: generateRandomName('email'),
+ label: 'E-mail'
+ }
+})
+
+export const Password = mergeStories(Default, {
+ play: buildTextWriterPlayFunction('qwerty'),
+ args: {
+ type: 'password',
+ name: generateRandomName('password'),
+ label: 'Password'
+ }
+})
+
+export const Required = mergeStories(Default, {
+ play: async (params) => {
+ const { canvasElement } = params
+ await buildTextWriterPlayFunction('some text')(params)
+
+ await wait(1000)
+
+ const canvas = within(canvasElement)
+ const input = canvas.getByRole('textbox')
+
+ userEvent.clear(input)
+
+ await wait(1000)
+
+ userEvent.tab()
+ },
+ args: {
+ name: generateRandomName('required'),
+ label: 'This one is required!',
+ showRequired: true,
+ rules: [(val: string) => (val ? true : 'This field is required')],
+ validateOnMount: true
+ }
+})
+
+export const Disabled = mergeStories(Default, {
+ play: buildTextWriterPlayFunction('12345'),
+ args: {
+ name: generateRandomName('disabled'),
+ label: 'Disabled input',
+ disabled: true
+ }
+})
+
+export const WithClear = mergeStories(Default, {
+ play: buildTextWriterPlayFunction('12345'),
+ args: {
+ name: generateRandomName('withclear'),
+ label: 'Click on cross to clear',
+ showClear: true
+ }
+})
+
+export const WithCustomRightSlot = mergeStories(Default, {
+ render: (args) => ({
+ components: { FormTextInput, FormButton },
+ setup() {
+ return { args }
+ },
+ template: ``
+ }),
+ play: buildTextWriterPlayFunction('12345'),
+ args: {
+ name: generateRandomName('withcustomrightslot'),
+ label: 'Right side is customized with a button!',
+ inputClasses: 'pr-20'
+ }
+})
diff --git a/packages/frontend-2/components/form/TextInput.vue b/packages/frontend-2/components/form/TextInput.vue
new file mode 100644
index 000000000..12461e977
--- /dev/null
+++ b/packages/frontend-2/components/form/TextInput.vue
@@ -0,0 +1,319 @@
+
+
+
+
+
+ {{ helpTip }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/file-upload/Progress.vue b/packages/frontend-2/components/form/file-upload/Progress.vue
new file mode 100644
index 000000000..d2928de0e
--- /dev/null
+++ b/packages/frontend-2/components/form/file-upload/Progress.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/file-upload/ProgressRow.vue b/packages/frontend-2/components/form/file-upload/ProgressRow.vue
new file mode 100644
index 000000000..00e1765e4
--- /dev/null
+++ b/packages/frontend-2/components/form/file-upload/ProgressRow.vue
@@ -0,0 +1,76 @@
+
+
+
+ {{ item.file.name }}
+
+ {{ prettyFileSize(item.file.size) }}
+
+
+
+
+
+
+ {{ item.file.name }}
+
+ {{ prettyFileSize(item.file.size) }}
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/file-upload/Zone.vue b/packages/frontend-2/components/form/file-upload/Zone.vue
new file mode 100644
index 000000000..f7ae3e8a0
--- /dev/null
+++ b/packages/frontend-2/components/form/file-upload/Zone.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/select/Base.stories.ts b/packages/frontend-2/components/form/select/Base.stories.ts
new file mode 100644
index 000000000..fa13dbce7
--- /dev/null
+++ b/packages/frontend-2/components/form/select/Base.stories.ts
@@ -0,0 +1,266 @@
+import { wait } from '@speckle/shared'
+import { Meta, StoryObj } from '@storybook/vue3'
+import FormSelectBase from '~~/components/form/select/Base.vue'
+import { isRequired } from '~~/lib/common/helpers/validation'
+
+type FakeItemType = { id: string; name: string }
+
+type StoryType = StoryObj<
+ Record & {
+ 'update:modelValue': (val: FakeItemType) => void
+ }
+>
+
+const fakeItems: FakeItemType[] = [
+ {
+ id: '1',
+ name: 'Rocky Balboa'
+ },
+ {
+ id: '2',
+ name: 'Bozo the Clown'
+ },
+ {
+ id: '3',
+ name: `Some jabroni with a super very long name, I mean look at it, it's way too long for a select box!`
+ },
+ {
+ id: '4',
+ name: 'Miss America 1987'
+ },
+ {
+ id: '5',
+ name: 'Brad Pitt'
+ },
+ {
+ id: '6',
+ name: 'Kevin McCallister'
+ },
+ {
+ id: '7',
+ name: 'Rickety Cricket'
+ },
+ {
+ id: '8',
+ name: 'Master Chief'
+ }
+]
+
+export default {
+ component: FormSelectBase,
+ parameters: {
+ docs: {
+ description: {
+ component: 'Base component for implementing various kinds of select boxes.'
+ }
+ }
+ },
+ argTypes: {
+ 'update:modelValue': {
+ type: 'function',
+ action: 'v-model'
+ },
+ 'nothing-selected': {
+ type: 'string',
+ description: 'Slot for rendering selectbox contents when nothing is selected'
+ },
+ 'something-selected': {
+ type: 'string',
+ description: 'Slot for rendering selectbox contents when something is selected'
+ },
+ option: {
+ type: 'string',
+ description: 'Slot for rendering each option'
+ },
+ filterPredicate: {
+ type: 'function'
+ },
+ getSearchResults: {
+ type: 'function'
+ },
+ disabled: {
+ type: 'boolean'
+ },
+ buttonStyle: {
+ options: ['base', 'simple'],
+ control: { type: 'select' }
+ }
+ }
+} as Meta
+
+export const Default: StoryType = {
+ render: (args, ctx) => ({
+ components: { FormSelectBase },
+ setup: () => {
+ return { args }
+ },
+ template: `
+
+
+
+ `,
+ methods: {
+ onModelUpdate(val: FakeItemType) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ }),
+ args: {
+ multiple: false,
+ items: fakeItems,
+ modelValue: undefined,
+ search: false,
+ filterPredicate: (val: FakeItemType, search: string) =>
+ val.name.toLowerCase().includes(search.toLowerCase()),
+ getSearchResults: undefined,
+ searchPlaceholder: 'Search',
+ label: 'Choose an item',
+ showLabel: false,
+ by: 'name',
+ name: 'example',
+ clearable: true,
+ buttonStyle: 'base',
+ disabled: false
+ }
+}
+
+export const LimitedWidth: StoryType = {
+ ...Default,
+ render: (args, ctx) => ({
+ components: { FormSelectBase },
+ setup: () => {
+ return { args }
+ },
+ template: `
+
+
+
+ `,
+ methods: {
+ onModelUpdate(val: FakeItemType) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ })
+}
+
+export const WithCustomSlots: StoryType = {
+ render: (args, ctx) => ({
+ components: { FormSelectBase },
+ setup: () => {
+ return { args }
+ },
+ template: `
+
+
+ {{ args['nothing-selected'] }}
+ {{ args['something-selected'] }}
+ {{ args['option'] }}
+
+
+ `,
+ methods: {
+ onModelUpdate(val: FakeItemType) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ }),
+ args: {
+ ...Default.args,
+ 'nothing-selected': 'NOTHING SELECTED SLOT',
+ 'something-selected': 'SOMETHING SELECTED SLOT',
+ option: 'OPTION SLOT'
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Use slots to change how various aspects render'
+ }
+ }
+ }
+}
+
+export const Disabled: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ disabled: true
+ }
+}
+
+export const WithValidation: StoryType = {
+ render: (args, ctx) => ({
+ components: { FormSelectBase },
+ setup: () => {
+ return { args }
+ },
+ template: `
+
+
+
+ `,
+ methods: {
+ onModelUpdate(val: FakeItemType) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ }),
+ args: {
+ multiple: false,
+ items: fakeItems,
+ modelValue: undefined,
+ search: false,
+ filterPredicate: (val: FakeItemType, search: string) =>
+ val.name.toLowerCase().includes(search.toLowerCase()),
+ searchPlaceholder: 'Search',
+ label: 'Item',
+ showLabel: true,
+ by: 'name',
+ rules: [isRequired],
+ help: 'This is a random help message'
+ }
+}
+
+export const WithFilter: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ search: true
+ }
+}
+
+export const WithAsyncSearch: StoryType = {
+ ...WithFilter,
+ args: {
+ ...WithFilter.args,
+ items: undefined,
+ filterPredicate: undefined,
+ getSearchResults: async (search: string): Promise => {
+ const items = fakeItems.filter((val) =>
+ val.name.toLowerCase().includes(search.toLowerCase())
+ )
+ await wait(2000)
+ return items
+ }
+ }
+}
+
+export const Empty: StoryType = {
+ ...WithAsyncSearch,
+ args: {
+ ...WithAsyncSearch.args,
+ getSearchResults: () => []
+ }
+}
+
+export const Simple: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ buttonStyle: 'simple'
+ }
+}
diff --git a/packages/frontend-2/components/form/select/Base.vue b/packages/frontend-2/components/form/select/Base.vue
new file mode 100644
index 000000000..c3abe3d48
--- /dev/null
+++ b/packages/frontend-2/components/form/select/Base.vue
@@ -0,0 +1,520 @@
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+ {{ simpleDisplayText(wrappedValue) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ helpTip }}
+
+
+
+
diff --git a/packages/frontend-2/components/form/select/ProjectRoles.vue b/packages/frontend-2/components/form/select/ProjectRoles.vue
new file mode 100644
index 000000000..a59238a6f
--- /dev/null
+++ b/packages/frontend-2/components/form/select/ProjectRoles.vue
@@ -0,0 +1,71 @@
+
+
+
+ {{ multiple ? 'Select roles' : 'Select role' }}
+
+
+
+
+
+
+ {{ roleDisplayName(item) + (i < value.length - 1 ? ', ' : '') }}
+
+
+
+ +{{ hiddenSelectedItemCount }}
+
+
+
+
+
+ {{ roleDisplayName(isArrayValue(value) ? value[0] : value) }}
+
+
+
+
+
+ {{ roleDisplayName(item) }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/select/Projects.vue b/packages/frontend-2/components/form/select/Projects.vue
new file mode 100644
index 000000000..f30036353
--- /dev/null
+++ b/packages/frontend-2/components/form/select/Projects.vue
@@ -0,0 +1,152 @@
+
+
+
+
+ {{ selectorPlaceholder }}
+
+
+ {{ multiple ? 'Select projects' : 'Select a project' }}
+
+
+
+
+
+
+
+ {{ item.name + (i < value.length - 1 ? ', ' : '') }}
+
+
+
+ +{{ hiddenSelectedItemCount }}
+
+
+
+
+
+
+ {{ (isArrayValue(value) ? value[0] : value).name }}
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/select/SourceApps.stories.ts b/packages/frontend-2/components/form/select/SourceApps.stories.ts
new file mode 100644
index 000000000..fb8c0794c
--- /dev/null
+++ b/packages/frontend-2/components/form/select/SourceApps.stories.ts
@@ -0,0 +1,75 @@
+import { Meta, StoryObj } from '@storybook/vue3'
+import FormSelectSourceApps from '~~/components/form/select/SourceApps.vue'
+
+type StoryType = StoryObj<
+ Record & {
+ 'update:modelValue': (val: unknown) => void
+ }
+>
+
+export default {
+ component: FormSelectSourceApps,
+ argTypes: {
+ 'update:modelValue': {
+ action: 'update:modelValue',
+ type: 'function'
+ },
+ 'nothing-selected': {
+ type: 'string',
+ description:
+ 'When nothing has been selected, you can use the slot to render the contents'
+ }
+ }
+} as Meta
+
+export const Default: StoryType = {
+ render: (args, ctx) => ({
+ components: { FormSelectSourceApps },
+ setup: () => {
+ return { args }
+ },
+ template: `
+
+
+
+ `,
+ methods: {
+ onModelUpdate(val: unknown) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ }),
+ args: {
+ search: false,
+ multiple: false,
+ modelValue: undefined,
+ label: 'Choose an app',
+ showLabel: false,
+ 'nothing-selected': undefined
+ }
+}
+
+export const Multiple: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ multiple: true
+ }
+}
+
+export const WithSearch: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ search: true
+ }
+}
+
+export const Disabled: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ disabled: true
+ }
+}
diff --git a/packages/frontend-2/components/form/select/SourceApps.vue b/packages/frontend-2/components/form/select/SourceApps.vue
new file mode 100644
index 000000000..79df55a8a
--- /dev/null
+++ b/packages/frontend-2/components/form/select/SourceApps.vue
@@ -0,0 +1,137 @@
+
+
+
+
+ {{ selectorPlaceholder }}
+
+
+ {{ multiple ? 'Select apps' : 'Select an app' }}
+
+
+
+
+
+
+
+
+
+ +{{ hiddenSelectedItemCount }}
+
+
+
+
+
+
+
{{ firstItem(value).name }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/form/select/Users.stories.ts b/packages/frontend-2/components/form/select/Users.stories.ts
new file mode 100644
index 000000000..81dbb9749
--- /dev/null
+++ b/packages/frontend-2/components/form/select/Users.stories.ts
@@ -0,0 +1,144 @@
+import { Meta, StoryObj } from '@storybook/vue3'
+import FormSelectUsers from '~~/components/form/select/Users.vue'
+import { FormUsersSelectItemFragment } from '~~/lib/common/generated/gql/graphql'
+import { ApolloMockData } from '~~/lib/common/helpers/storybook'
+
+type StoryType = StoryObj<
+ Record & {
+ 'update:modelValue': (val: FormUsersSelectItemFragment) => void
+ }
+>
+
+export const fakeUsers: ApolloMockData = [
+ {
+ __typename: 'LimitedUser',
+ id: '1',
+ name: 'Rocky Balboa',
+ avatar:
+ 'https://www.gannett-cdn.com/-mm-/ea8a07dc617309ca168e259d006a72abae509118/c=0-35-650-402/local/-/media/2016/02/25/SiouxFalls/SiouxFalls/635919633708862216-rocky.jpg?width=1200&disable=upscale&format=pjpg&auto=webp'
+ },
+ {
+ __typename: 'LimitedUser',
+ id: '2',
+ name: 'Bozo the Clown',
+ avatar:
+ 'https://cdn.vox-cdn.com/thumbor/Z2b-41HMCuVhqtEAkgor1w5iy-E=/1400x1050/filters:format(jpeg)/cdn.vox-cdn.com/uploads/chorus_asset/file/10483479/bozo_RIP_getty_ringer.jpg'
+ },
+ {
+ __typename: 'LimitedUser',
+ id: '3',
+ name: `Some jabroni with a super very long name, I mean look at it, it's way too long for a select box!`,
+ avatar: null
+ },
+ {
+ __typename: 'LimitedUser',
+ id: '4',
+ name: 'Miss America 1987',
+ avatar: null
+ },
+ {
+ __typename: 'LimitedUser',
+ id: '5',
+ name: 'Brad Pitt',
+ avatar:
+ 'https://media1.popsugar-assets.com/files/thumbor/4UYUg9UKWqqhaFfElFDU9bKMRgQ/356x1145:1857x2646/fit-in/500x500/filters:format_auto-!!-:strip_icc-!!-/2019/09/04/970/n/1922398/cc3fa7b15d70381d55bd82.88203803_/i/Brad-Pitt.jpg'
+ },
+ {
+ __typename: 'LimitedUser',
+ id: '6',
+ name: 'Kevin McCallister',
+ avatar:
+ 'https://m.media-amazon.com/images/M/MV5BZjg2ODUwZTgtODRkMS00N2U1LTg2Y2EtNDVhMjRmMDNkNDk3XkEyXkFqcGdeQWFybm8@._V1_.jpg'
+ },
+ {
+ __typename: 'LimitedUser',
+ id: '7',
+ name: 'Rickety Cricket',
+ avatar: 'https://cdn3.whatculture.com/images/2022/08/4ce6e7d99a9761f6-1200x675.jpg'
+ },
+ {
+ __typename: 'LimitedUser',
+ id: '8',
+ name: 'Master Chief',
+ avatar:
+ 'https://cdn1.dotesports.com/wp-content/uploads/2021/08/09111246/MasterChief.jpg'
+ },
+ {
+ __typename: 'LimitedUser',
+ id: '9',
+ name: 'Mario',
+ avatar:
+ 'https://play-lh.googleusercontent.com/5LIMaa7WTNy34bzdFhBETa2MRj7mFJZWb8gCn_uyxQkUvFx_uOFCeQjcK16c6WpBA3E'
+ }
+]
+
+export default {
+ component: FormSelectUsers,
+ argTypes: {
+ 'update:modelValue': {
+ action: 'update:modelValue',
+ type: 'function'
+ },
+ 'nothing-selected': {
+ type: 'string',
+ description:
+ 'When nothing has been selected, you can use the slot to render the contents'
+ }
+ },
+ excludeStories: ['fakeUsers']
+} as Meta
+
+export const Default: StoryType = {
+ render: (args, ctx) => ({
+ components: { FormSelectUsers },
+ setup: () => {
+ const selectedUser = ref(undefined)
+ return { args, selectedUser }
+ },
+ template: `
+
+
+
+ `,
+ methods: {
+ onModelUpdate(val: FormUsersSelectItemFragment) {
+ args['update:modelValue'](val)
+ ctx.updateArgs({ ...args, modelValue: val })
+ }
+ }
+ }),
+ args: {
+ search: false,
+ multiple: false,
+ users: fakeUsers,
+ modelValue: undefined,
+ label: 'Choose a user',
+ showLabel: false,
+ 'nothing-selected': undefined
+ }
+}
+
+export const Multiple: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ multiple: true,
+ 'nothing-selected': 'Choose multiple users'
+ }
+}
+
+export const WithSearch: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ search: true
+ }
+}
+
+export const Disabled: StoryType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ disabled: true
+ }
+}
diff --git a/packages/frontend-2/components/form/select/Users.vue b/packages/frontend-2/components/form/select/Users.vue
new file mode 100644
index 000000000..ae101b147
--- /dev/null
+++ b/packages/frontend-2/components/form/select/Users.vue
@@ -0,0 +1,151 @@
+
+
+
+
+ {{ selectorPlaceholder }}
+
+
+ {{ multiple ? 'Select users' : 'Select a user' }}
+
+
+
+
+
+
+
+
+
+ +{{ hiddenSelectedItemCount }}
+
+
+
+
+
+
+
+ {{ (isArrayValue(value) ? value[0] : value).name }}
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/global/AvatarGroup.vue b/packages/frontend-2/components/global/AvatarGroup.vue
new file mode 100644
index 000000000..60e6f76a7
--- /dev/null
+++ b/packages/frontend-2/components/global/AvatarGroup.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+ {{ user.name.split(' ')[0][0] }}{{ user.name.split(' ')[1][0] }}
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/global/HelpText.vue b/packages/frontend-2/components/global/HelpText.vue
new file mode 100644
index 000000000..cf958bfc2
--- /dev/null
+++ b/packages/frontend-2/components/global/HelpText.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/global/InfiniteLoading.vue b/packages/frontend-2/components/global/InfiniteLoading.vue
new file mode 100644
index 000000000..c07e2420a
--- /dev/null
+++ b/packages/frontend-2/components/global/InfiniteLoading.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+ That's it, you've loaded everything!
+
+
+
+
+
+
+
+ An error occurred while loading
+
+
Retry
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/global/SourceAppBadge.vue b/packages/frontend-2/components/global/SourceAppBadge.vue
new file mode 100644
index 000000000..811e5b62c
--- /dev/null
+++ b/packages/frontend-2/components/global/SourceAppBadge.vue
@@ -0,0 +1,16 @@
+
+
+ {{ sourceApp.short }}
+
+
+
diff --git a/packages/frontend-2/components/global/icon/Cursor.vue b/packages/frontend-2/components/global/icon/Cursor.vue
new file mode 100644
index 000000000..05496646f
--- /dev/null
+++ b/packages/frontend-2/components/global/icon/Cursor.vue
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/frontend-2/components/global/icon/FileExplorer.vue b/packages/frontend-2/components/global/icon/FileExplorer.vue
new file mode 100644
index 000000000..c25302696
--- /dev/null
+++ b/packages/frontend-2/components/global/icon/FileExplorer.vue
@@ -0,0 +1,14 @@
+
+
+
diff --git a/packages/frontend-2/components/global/icon/Perspective.vue b/packages/frontend-2/components/global/icon/Perspective.vue
new file mode 100644
index 000000000..ad399bcbc
--- /dev/null
+++ b/packages/frontend-2/components/global/icon/Perspective.vue
@@ -0,0 +1,13 @@
+
+
+
diff --git a/packages/frontend-2/components/global/icon/PerspectiveMore.vue b/packages/frontend-2/components/global/icon/PerspectiveMore.vue
new file mode 100644
index 000000000..9da56bdb1
--- /dev/null
+++ b/packages/frontend-2/components/global/icon/PerspectiveMore.vue
@@ -0,0 +1,13 @@
+
+
+
diff --git a/packages/frontend-2/components/global/logo/TextWhite.vue b/packages/frontend-2/components/global/logo/TextWhite.vue
new file mode 100644
index 000000000..ee13ab70a
--- /dev/null
+++ b/packages/frontend-2/components/global/logo/TextWhite.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/packages/frontend-2/components/header/LogoBlock.vue b/packages/frontend-2/components/header/LogoBlock.vue
new file mode 100644
index 000000000..3ef4b3514
--- /dev/null
+++ b/packages/frontend-2/components/header/LogoBlock.vue
@@ -0,0 +1,28 @@
+
+
+
+
+ Speckle
+
+
+
+
diff --git a/packages/frontend-2/components/header/NavBar.vue b/packages/frontend-2/components/header/NavBar.vue
new file mode 100644
index 000000000..177c81345
--- /dev/null
+++ b/packages/frontend-2/components/header/NavBar.vue
@@ -0,0 +1,35 @@
+
+
+
+
diff --git a/packages/frontend-2/components/header/NavLink.vue b/packages/frontend-2/components/header/NavLink.vue
new file mode 100644
index 000000000..a2035c275
--- /dev/null
+++ b/packages/frontend-2/components/header/NavLink.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ {{ name || to }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/header/NavNotifications.vue b/packages/frontend-2/components/header/NavNotifications.vue
new file mode 100644
index 000000000..9bfafaf58
--- /dev/null
+++ b/packages/frontend-2/components/header/NavNotifications.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/header/NavUserMenu.vue b/packages/frontend-2/components/header/NavUserMenu.vue
new file mode 100644
index 000000000..c00af3f1c
--- /dev/null
+++ b/packages/frontend-2/components/header/NavUserMenu.vue
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/header/ThemeToggle.vue b/packages/frontend-2/components/header/ThemeToggle.vue
new file mode 100644
index 000000000..56275e036
--- /dev/null
+++ b/packages/frontend-2/components/header/ThemeToggle.vue
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/packages/frontend-2/components/layout/Dialog.vue b/packages/frontend-2/components/layout/Dialog.vue
new file mode 100644
index 000000000..0aec2834b
--- /dev/null
+++ b/packages/frontend-2/components/layout/Dialog.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/layout/Disclosure.vue b/packages/frontend-2/components/layout/Disclosure.vue
new file mode 100644
index 000000000..b55112008
--- /dev/null
+++ b/packages/frontend-2/components/layout/Disclosure.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+ Panel contents
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/layout/GridListToggle.vue b/packages/frontend-2/components/layout/GridListToggle.vue
new file mode 100644
index 000000000..7f338a065
--- /dev/null
+++ b/packages/frontend-2/components/layout/GridListToggle.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/layout/Menu.vue b/packages/frontend-2/components/layout/Menu.vue
new file mode 100644
index 000000000..5de4e8af9
--- /dev/null
+++ b/packages/frontend-2/components/layout/Menu.vue
@@ -0,0 +1,124 @@
+
+
+
+
diff --git a/packages/frontend-2/components/layout/Panel.vue b/packages/frontend-2/components/layout/Panel.vue
new file mode 100644
index 000000000..a15496916
--- /dev/null
+++ b/packages/frontend-2/components/layout/Panel.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/layout/Tabs.vue b/packages/frontend-2/components/layout/Tabs.vue
new file mode 100644
index 000000000..7183e7b12
--- /dev/null
+++ b/packages/frontend-2/components/layout/Tabs.vue
@@ -0,0 +1,35 @@
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/notifications/DashboardList.vue b/packages/frontend-2/components/notifications/DashboardList.vue
new file mode 100644
index 000000000..fee158528
--- /dev/null
+++ b/packages/frontend-2/components/notifications/DashboardList.vue
@@ -0,0 +1,51 @@
+
+
+
Pending Invites
+
+
Check out the Design System.
+
+ Take me there
+
+
+
+
+
+ {{ notification.message }}
+
+
+
+ {{ action }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/onboarding/checklist/v1.vue b/packages/frontend-2/components/onboarding/checklist/v1.vue
new file mode 100644
index 000000000..6bbc4bd81
--- /dev/null
+++ b/packages/frontend-2/components/onboarding/checklist/v1.vue
@@ -0,0 +1,328 @@
+
+
+
+
+
+
+
Quickstart Checklist
+
+ Become a Speckle pro in four steps!
+
+
+
+ I'll do it later
+
+
+
+
+
+
+ {{ idx + 1 }}
+
+
+
+
+ {{ step.title }}
+
+
{{ step.blurb }}
+
+
+
+ {{ step.cta }}
+
+
+
+
+
+
+ Completed!
+
+ {{ step.postCompletionCta }}
+
+
+
+
+ Complete the previous step!
+
+
+
+
+
+
+
+
+
+
+
+
+ All done!
+
+ Don't forget to join us in the
+
+ Community Forum
+
+ , or check out the
+
+ Tutorials
+
+ page for more inisghts.
+
+
Close
+
+
+
+
+
+
+ I'll do it later - let me explore first!
+
+
+
+
+
+ Desktop Login
+
+
+ Your First Upload
+
+
(!v ? markComplete(3) : '')"
+ />
+
+
+
diff --git a/packages/frontend-2/components/onboarding/dialog/AccountLink.vue b/packages/frontend-2/components/onboarding/dialog/AccountLink.vue
new file mode 100644
index 000000000..5e1d298fb
--- /dev/null
+++ b/packages/frontend-2/components/onboarding/dialog/AccountLink.vue
@@ -0,0 +1,34 @@
+
+
+ One step away from sending a model to Speckle!
+
+
+
Install manager + How and why to login into manager
+
+
+
+
+ Authorize Manager
+
+
+
+
+
diff --git a/packages/frontend-2/components/onboarding/dialog/Base.vue b/packages/frontend-2/components/onboarding/dialog/Base.vue
new file mode 100644
index 000000000..df84b6113
--- /dev/null
+++ b/packages/frontend-2/components/onboarding/dialog/Base.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+ Dialog Header
+
+
+
+
+
+ {{ cancelPrompt }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/onboarding/dialog/FirstSend.vue b/packages/frontend-2/components/onboarding/dialog/FirstSend.vue
new file mode 100644
index 000000000..71e4e2a21
--- /dev/null
+++ b/packages/frontend-2/components/onboarding/dialog/FirstSend.vue
@@ -0,0 +1,17 @@
+
+
+ How to do a first model upload
+
+
+
How to do a first model upload
+
+
+ Got it!
+
+
+
+
diff --git a/packages/frontend-2/components/onboarding/dialog/Manager.vue b/packages/frontend-2/components/onboarding/dialog/Manager.vue
new file mode 100644
index 000000000..8cc2edf1f
--- /dev/null
+++ b/packages/frontend-2/components/onboarding/dialog/Manager.vue
@@ -0,0 +1,113 @@
+
+
+ Ready to send your first model?
+
+
+
How and why to download manager
+
+
+
+ Download For {{ os }}
+
+
+ Download for {{ os === 'Windows' ? 'Mac OS' : 'Windows' }}
+
+
+
+
+ Speckle Connectors exist only for applications running on Windows or Mac OS. If
+ you want, you can still go ahead and download Speckle Manager for
+
+ Windows
+
+ or
+
+ Mac OS
+
+ .
+
+
+
+
+
diff --git a/packages/frontend-2/components/preview/Image.vue b/packages/frontend-2/components/preview/Image.vue
new file mode 100644
index 000000000..cfb370555
--- /dev/null
+++ b/packages/frontend-2/components/preview/Image.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+ calculatePanoramaStyle(e)"
+ @touchmove="(e:TouchEvent) =>
+ calculatePanoramaStyle({
+ target: e.target,
+ clientX: e.touches[0].clientX,
+ clientY: e.touches[0].clientY
+ } as MouseEvent)"
+ />
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/CardImportFileArea.vue b/packages/frontend-2/components/project/CardImportFileArea.vue
new file mode 100644
index 000000000..b079015f2
--- /dev/null
+++ b/packages/frontend-2/components/project/CardImportFileArea.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+ {{ fileUpload.file.name }}
+
+
+
+ {{ errorMessage }}
+
+
+
+
+ Use our
+ connectors
+ to send data to this model, or drag and drop a IFC/OBJ/STL file here.
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/CommentPermissionsSelect.vue b/packages/frontend-2/components/project/CommentPermissionsSelect.vue
new file mode 100644
index 000000000..c922942d6
--- /dev/null
+++ b/packages/frontend-2/components/project/CommentPermissionsSelect.vue
@@ -0,0 +1,54 @@
+
+
+
+
+ {{ value.title }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/PendingFileImportStatus.vue b/packages/frontend-2/components/project/PendingFileImportStatus.vue
new file mode 100644
index 000000000..b4bff31df
--- /dev/null
+++ b/packages/frontend-2/components/project/PendingFileImportStatus.vue
@@ -0,0 +1,53 @@
+
+
+
+ {{ isSelfImport ? 'Importing' : 'Importing new version' }}
+
+
+
+
+
+
+ {{ isSelfImport ? 'Import successful' : 'Version import successful' }}
+
+
+
+
+
+
+ {{ isSelfImport ? 'Import failed' : 'Version import failed' }}
+
+
+ {{ upload.convertedMessage }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/VisibilitySelect.vue b/packages/frontend-2/components/project/VisibilitySelect.vue
new file mode 100644
index 000000000..c1e52e11a
--- /dev/null
+++ b/packages/frontend-2/components/project/VisibilitySelect.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+ {{ value.title }}
+
+
{{ value.description }}
+
+
+
+
+
{{ item.title }}
+
{{ item.description }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/discussions-page/Header.vue b/packages/frontend-2/components/project/discussions-page/Header.vue
new file mode 100644
index 000000000..c3a262e48
--- /dev/null
+++ b/packages/frontend-2/components/project/discussions-page/Header.vue
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/discussions-page/Results.vue b/packages/frontend-2/components/project/discussions-page/Results.vue
new file mode 100644
index 000000000..fb5a07bf3
--- /dev/null
+++ b/packages/frontend-2/components/project/discussions-page/Results.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
TODO: No threads
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/Header.vue b/packages/frontend-2/components/project/model-page/Header.vue
new file mode 100644
index 000000000..466bf38dd
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/Header.vue
@@ -0,0 +1,37 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/Versions.vue b/packages/frontend-2/components/project/model-page/Versions.vue
new file mode 100644
index 000000000..26e33e9c9
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/Versions.vue
@@ -0,0 +1,260 @@
+
+
+
Versions
+
+
+ {{ `${selectedItems.length} version${selectedItems.length > 1 ? 's' : ''}` }}
+ selected
+
+
+
+ Clear selection
+
+
+ Move to
+
+ Delete
+
+
+
+
+
+
TODO: Versions Empty state
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/dialog/Delete.vue b/packages/frontend-2/components/project/model-page/dialog/Delete.vue
new file mode 100644
index 000000000..7783c344b
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/dialog/Delete.vue
@@ -0,0 +1,78 @@
+
+
+
+
+ Delete {{ `${versions.length} version${versions.length > 1 ? 's' : ''}` }}
+
+
+ Deleting versions is an irrevocable action! If you are sure about wanting to
+ delete
+ the selected versions,
+
+ the selected version
+ "{{ versions[0].message }}",
+
+ please click on the button below!
+
+
+
+ Delete
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/dialog/EditMessage.vue b/packages/frontend-2/components/project/model-page/dialog/EditMessage.vue
new file mode 100644
index 000000000..eeae82113
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/dialog/EditMessage.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/dialog/MoveTo.vue b/packages/frontend-2/components/project/model-page/dialog/MoveTo.vue
new file mode 100644
index 000000000..3320bdecf
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/dialog/MoveTo.vue
@@ -0,0 +1,82 @@
+
+
+
+
+ Move {{ `${versions.length} version${versions.length > 1 ? 's' : ''}` }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/dialog/move-to/ExistingTab.vue b/packages/frontend-2/components/project/model-page/dialog/move-to/ExistingTab.vue
new file mode 100644
index 000000000..35a9c4ab7
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/dialog/move-to/ExistingTab.vue
@@ -0,0 +1,53 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/dialog/move-to/NewTab.vue b/packages/frontend-2/components/project/model-page/dialog/move-to/NewTab.vue
new file mode 100644
index 000000000..3893cec92
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/dialog/move-to/NewTab.vue
@@ -0,0 +1,50 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/versions/Card.vue b/packages/frontend-2/components/project/model-page/versions/Card.vue
new file mode 100644
index 000000000..5a14296cb
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/versions/Card.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ version.commentThreadCount.totalCount }}
+
+
+
+
+
+ created
+ {{ createdAt }}
+
+
+
+
+ {{ message }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/model-page/versions/CardActions.vue b/packages/frontend-2/components/project/model-page/versions/CardActions.vue
new file mode 100644
index 000000000..be363b063
--- /dev/null
+++ b/packages/frontend-2/components/project/model-page/versions/CardActions.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/models-page/Header.vue b/packages/frontend-2/components/project/models-page/Header.vue
new file mode 100644
index 000000000..c572691e1
--- /dev/null
+++ b/packages/frontend-2/components/project/models-page/Header.vue
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
All Models
+
+
+ View all in 3D
+
+
+ New
+
+
+
+
+
updateSearchImmediately($event.value)"
+ @update:model-value="updateDebouncedSearch"
+ >
+
+
+
+
+
+
+
+ View all in 3D
+
+
+ New
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/models-page/Results.vue b/packages/frontend-2/components/project/models-page/Results.vue
new file mode 100644
index 000000000..298485fbb
--- /dev/null
+++ b/packages/frontend-2/components/project/models-page/Results.vue
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/Header.vue b/packages/frontend-2/components/project/page/Header.vue
new file mode 100644
index 000000000..42502c36d
--- /dev/null
+++ b/packages/frontend-2/components/project/page/Header.vue
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/LatestItems.vue b/packages/frontend-2/components/project/page/LatestItems.vue
new file mode 100644
index 000000000..9d331bf7a
--- /dev/null
+++ b/packages/frontend-2/components/project/page/LatestItems.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+ {{ count }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/MoreActions.vue b/packages/frontend-2/components/project/page/MoreActions.vue
new file mode 100644
index 000000000..5f9083f53
--- /dev/null
+++ b/packages/frontend-2/components/project/page/MoreActions.vue
@@ -0,0 +1,25 @@
+
+
+
Automation
+
+
+
+ Webhooks
+
+ If you need to use webhooks, ask the stream's owner to grant you ownership.
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/StatsBlock.vue b/packages/frontend-2/components/project/page/StatsBlock.vue
new file mode 100644
index 000000000..af22cebf5
--- /dev/null
+++ b/packages/frontend-2/components/project/page/StatsBlock.vue
@@ -0,0 +1,13 @@
+
+
+
diff --git a/packages/frontend-2/components/project/page/latest-items/Comments.vue b/packages/frontend-2/components/project/page/latest-items/Comments.vue
new file mode 100644
index 000000000..85b737486
--- /dev/null
+++ b/packages/frontend-2/components/project/page/latest-items/Comments.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/latest-items/Models.vue b/packages/frontend-2/components/project/page/latest-items/Models.vue
new file mode 100644
index 000000000..713cde1af
--- /dev/null
+++ b/packages/frontend-2/components/project/page/latest-items/Models.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+ updateSearchImmediately($event.value)"
+ @update:model-value="updateDebouncedSearch"
+ >
+
+
+
+
+ View all in 3D
+
+
+ New Model
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/latest-items/comments/Grid.vue b/packages/frontend-2/components/project/page/latest-items/comments/Grid.vue
new file mode 100644
index 000000000..0a6b39c46
--- /dev/null
+++ b/packages/frontend-2/components/project/page/latest-items/comments/Grid.vue
@@ -0,0 +1,29 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue b/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue
new file mode 100644
index 000000000..9936b5899
--- /dev/null
+++ b/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ thread.rawText }}
+
+
+ {{ thread.repliesCount.totalCount }}
+ {{ thread.repliesCount.totalCount === 1 ? 'reply' : 'replies' }}
+
+
+ {{ updatedAt }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/latest-items/comments/IntroCard.vue b/packages/frontend-2/components/project/page/latest-items/comments/IntroCard.vue
new file mode 100644
index 000000000..263811413
--- /dev/null
+++ b/packages/frontend-2/components/project/page/latest-items/comments/IntroCard.vue
@@ -0,0 +1,29 @@
+
+
+

+
+
No comments to show
+
+
+ Read this tutorial and be the first person to comment
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/latest-items/comments/List.vue b/packages/frontend-2/components/project/page/latest-items/comments/List.vue
new file mode 100644
index 000000000..f616f3670
--- /dev/null
+++ b/packages/frontend-2/components/project/page/latest-items/comments/List.vue
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue b/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue
new file mode 100644
index 000000000..0bc4d8830
--- /dev/null
+++ b/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+ {{ thread.author.name }}
+
+ & {{ thread.replyAuthors.totalCount }} others
+
+
+
+
+ {{ thread.rawText }}
+
+
+
+
+
+ {{ updatedAt }}
+
+ {{ thread.repliesCount.totalCount }}
+ {{ thread.repliesCount.totalCount === 1 ? 'reply' : 'replies' }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/Actions.vue b/packages/frontend-2/components/project/page/models/Actions.vue
new file mode 100644
index 000000000..cb8e4cbea
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/Actions.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/Card.vue b/packages/frontend-2/components/project/page/models/Card.vue
new file mode 100644
index 000000000..85739b42f
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/Card.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+ {{ nameParts[0] }}
+
+
+ {{ nameParts[1] }}
+
+
+
+
+ updated
+ {{ updatedAt }}
+
+
+
+ {{ versionCount }}
+
+
+
+
+
+
+ {{ model.commentThreadCount.totalCount }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/CardView.vue b/packages/frontend-2/components/project/page/models/CardView.vue
new file mode 100644
index 000000000..2579ef8f5
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/CardView.vue
@@ -0,0 +1,176 @@
+
+
+
+
$emit('model-clicked', { id: item.id, e: $event })"
+ />
+
+ $emit('clear-search')"
+ />
+ TODO: Grid empty state
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/ListView.vue b/packages/frontend-2/components/project/page/models/ListView.vue
new file mode 100644
index 000000000..1d14740ec
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/ListView.vue
@@ -0,0 +1,201 @@
+
+
+
+ TODO: List empty state
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/NewDialog.vue b/packages/frontend-2/components/project/page/models/NewDialog.vue
new file mode 100644
index 000000000..ba44741e5
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/NewDialog.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/NewModelStructureItem.vue b/packages/frontend-2/components/project/page/models/NewModelStructureItem.vue
new file mode 100644
index 000000000..d06159400
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/NewModelStructureItem.vue
@@ -0,0 +1,81 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/Preview.vue b/packages/frontend-2/components/project/page/models/Preview.vue
new file mode 100644
index 000000000..ed29710bc
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/Preview.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/StructureItem.vue b/packages/frontend-2/components/project/page/models/StructureItem.vue
new file mode 100644
index 000000000..fe093a3d0
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/StructureItem.vue
@@ -0,0 +1,379 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ submodel
+
+
+
+
+
+
+
+ updated
+ {{ updatedAt }}
+
+
+ {{ model?.commentThreadCount.totalCount }}
+
+
+
+
+ {{ model?.versionCount.totalCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/card/DeleteDialog.vue b/packages/frontend-2/components/project/page/models/card/DeleteDialog.vue
new file mode 100644
index 000000000..d5ae9c582
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/card/DeleteDialog.vue
@@ -0,0 +1,61 @@
+
+
+
+
Delete model
+
+ Are you sure you want to delete the model
+ {{ model.name }}
+ ? This action is irreversible and all of the versions inside of this model will
+ be deleted also!
+
+
+
+ Delete
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/models/card/RenameDialog.vue b/packages/frontend-2/components/project/page/models/card/RenameDialog.vue
new file mode 100644
index 000000000..3798e5022
--- /dev/null
+++ b/packages/frontend-2/components/project/page/models/card/RenameDialog.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/more-actions/Card.vue b/packages/frontend-2/components/project/page/more-actions/Card.vue
new file mode 100644
index 000000000..35881a251
--- /dev/null
+++ b/packages/frontend-2/components/project/page/more-actions/Card.vue
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/stats-block/Comments.vue b/packages/frontend-2/components/project/page/stats-block/Comments.vue
new file mode 100644
index 000000000..5707007c6
--- /dev/null
+++ b/packages/frontend-2/components/project/page/stats-block/Comments.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+ Threads
+
+
+
+
+ {{ project.commentThreadCount.totalCount }}
+
+ 0
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/stats-block/Models.vue b/packages/frontend-2/components/project/page/stats-block/Models.vue
new file mode 100644
index 000000000..92616f800
--- /dev/null
+++ b/packages/frontend-2/components/project/page/stats-block/Models.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+ Models
+
+
+
+
+ {{ project.modelCount.totalCount }}
+
+ 0
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/stats-block/Team.vue b/packages/frontend-2/components/project/page/stats-block/Team.vue
new file mode 100644
index 000000000..85122769a
--- /dev/null
+++ b/packages/frontend-2/components/project/page/stats-block/Team.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+ Team
+
+
{{ project.role?.split(':').reverse()[0] }}
+
+
+
+
+
+
+
+
+ {{ project.role === 'stream:owner' ? 'Manage' : 'View' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/stats-block/Versions.vue b/packages/frontend-2/components/project/page/stats-block/Versions.vue
new file mode 100644
index 000000000..695512a5c
--- /dev/null
+++ b/packages/frontend-2/components/project/page/stats-block/Versions.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ {{ project.versionCount.totalCount }}
+
+ 0
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/team/Dialog.vue b/packages/frontend-2/components/project/page/team/Dialog.vue
new file mode 100644
index 000000000..61aa02067
--- /dev/null
+++ b/packages/frontend-2/components/project/page/team/Dialog.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/team/PermissionSelect.vue b/packages/frontend-2/components/project/page/team/PermissionSelect.vue
new file mode 100644
index 000000000..dd684223c
--- /dev/null
+++ b/packages/frontend-2/components/project/page/team/PermissionSelect.vue
@@ -0,0 +1,78 @@
+
+
+
+
+ {{ value.title }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/team/dialog/DangerZones.vue b/packages/frontend-2/components/project/page/team/dialog/DangerZones.vue
new file mode 100644
index 000000000..ce6a40a18
--- /dev/null
+++ b/packages/frontend-2/components/project/page/team/dialog/DangerZones.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+ As long as you're not the only owner you can remove yourself from this
+ project's list of collaborators.
+
+ Removing yourself from the collaborators list is an irreversible action
+
+ and the only way you can get back on the list is if a project owner
+ invites you back.
+
+
+ Leave
+
+
+
+
+
+
+
+
+
+
+ Delete Project
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/team/dialog/InviteUser.vue b/packages/frontend-2/components/project/page/team/dialog/InviteUser.vue
new file mode 100644
index 000000000..1dc9ae16b
--- /dev/null
+++ b/packages/frontend-2/components/project/page/team/dialog/InviteUser.vue
@@ -0,0 +1,134 @@
+
+
+
+
+ Invite
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ user.name }}
+
+ Invite
+
+
+
+
+
+
+
+ {{ selectedEmails.join(', ') }}
+ onInviteUser(selectedEmails || [])"
+ >
+ Invite
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/team/dialog/ManagePermissions.vue b/packages/frontend-2/components/project/page/team/dialog/ManagePermissions.vue
new file mode 100644
index 000000000..17d8b0ae1
--- /dev/null
+++ b/packages/frontend-2/components/project/page/team/dialog/ManagePermissions.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+ Access
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/project/page/team/dialog/ManageUsers.vue b/packages/frontend-2/components/project/page/team/dialog/ManageUsers.vue
new file mode 100644
index 000000000..e2b00ba5d
--- /dev/null
+++ b/packages/frontend-2/components/project/page/team/dialog/ManageUsers.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+ Team
+
+
+
+
+
{{ collaborator.title }}
+
+
+
+
+ {{ roleSelectItems[collaborator.role].title }}
+
+
+
+
+ {{ roleSelectItems[collaborator.role].title }}
+
+
+ Cancel Invite
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/projects/AddDialog.vue b/packages/frontend-2/components/projects/AddDialog.vue
new file mode 100644
index 000000000..78d6aaf52
--- /dev/null
+++ b/packages/frontend-2/components/projects/AddDialog.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/projects/Dashboard.vue b/packages/frontend-2/components/projects/Dashboard.vue
new file mode 100644
index 000000000..cd44615dd
--- /dev/null
+++ b/packages/frontend-2/components/projects/Dashboard.vue
@@ -0,0 +1,289 @@
+
+
+
+
diff --git a/packages/frontend-2/components/projects/DashboardEmptyState.vue b/packages/frontend-2/components/projects/DashboardEmptyState.vue
new file mode 100644
index 000000000..f62368c34
--- /dev/null
+++ b/packages/frontend-2/components/projects/DashboardEmptyState.vue
@@ -0,0 +1,122 @@
+
+
+
+ Welcome to Speckle. What would you like to do?
+
+
+
+ Explore your first project
+
+ Create your first Speckle project and learn the basics
+
+ Explore
+
+
+ Already have a 3D model?
+
+ Install connectors from Revit, Rhino, AutoCAD, Blender and many others!
+
+ Download manager
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/projects/DashboardEmptyStatePanel.vue b/packages/frontend-2/components/projects/DashboardEmptyStatePanel.vue
new file mode 100644
index 000000000..17400c328
--- /dev/null
+++ b/packages/frontend-2/components/projects/DashboardEmptyStatePanel.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/projects/DashboardFilled.vue b/packages/frontend-2/components/projects/DashboardFilled.vue
new file mode 100644
index 000000000..4165b3c39
--- /dev/null
+++ b/packages/frontend-2/components/projects/DashboardFilled.vue
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/packages/frontend-2/components/projects/ProjectDashboardCard.vue b/packages/frontend-2/components/projects/ProjectDashboardCard.vue
new file mode 100644
index 000000000..7db7a0dd6
--- /dev/null
+++ b/packages/frontend-2/components/projects/ProjectDashboardCard.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+ {{ project.name }}
+
+
+
+
+
+
+ {{ project.role?.split(':').reverse()[0] }}
+
+
+
+
+
+ updated
+ {{ updatedAt }}
+
+
+
+
+ +{{ modelItemTotalCount - 4 }} model{{
+ modelItemTotalCount - 4 !== 1 ? 's' : ''
+ }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/projects/invite/Banner.vue b/packages/frontend-2/components/projects/invite/Banner.vue
new file mode 100644
index 000000000..b197509b8
--- /dev/null
+++ b/packages/frontend-2/components/projects/invite/Banner.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+ {{ invite.invitedBy.name }}
+ has invited you to be part of the team from
+ the project {{ invite.projectName }}.
+ this project.
+
+
+
+
+
+ Decline
+
+
+ Accept
+
+
+
+
+ Log In
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/projects/invite/Banners.vue b/packages/frontend-2/components/projects/invite/Banners.vue
new file mode 100644
index 000000000..4c0878e83
--- /dev/null
+++ b/packages/frontend-2/components/projects/invite/Banners.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/server/InviteDialog.vue b/packages/frontend-2/components/server/InviteDialog.vue
new file mode 100644
index 000000000..fa895f26d
--- /dev/null
+++ b/packages/frontend-2/components/server/InviteDialog.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/singleton/Managers.vue b/packages/frontend-2/components/singleton/Managers.vue
new file mode 100644
index 000000000..27dc2158d
--- /dev/null
+++ b/packages/frontend-2/components/singleton/Managers.vue
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/packages/frontend-2/components/singleton/ToastManager.vue b/packages/frontend-2/components/singleton/ToastManager.vue
new file mode 100644
index 000000000..4606b77f6
--- /dev/null
+++ b/packages/frontend-2/components/singleton/ToastManager.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentNotification.title }}
+
+
+ {{ currentNotification.description }}
+
+
+
+ {{ currentNotification.cta.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/tour/Comment.vue b/packages/frontend-2/components/tour/Comment.vue
new file mode 100644
index 000000000..abbeb80e8
--- /dev/null
+++ b/packages/frontend-2/components/tour/Comment.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+ Skip
+
+
+
+ Previous
+
+
+ Next
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/tour/Onboarding.vue b/packages/frontend-2/components/tour/Onboarding.vue
new file mode 100644
index 000000000..60884797b
--- /dev/null
+++ b/packages/frontend-2/components/tour/Onboarding.vue
@@ -0,0 +1,29 @@
+
+
+
+
diff --git a/packages/frontend-2/components/tour/Segmentation.vue b/packages/frontend-2/components/tour/Segmentation.vue
new file mode 100644
index 000000000..1fce0b0f0
--- /dev/null
+++ b/packages/frontend-2/components/tour/Segmentation.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+ Welcome, {{ activeUser?.name?.split(' ')[0] }}!
+
+
+ Let's get to know each other. What industry do you work in?
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
Thanks!
+
One last thing. What's your job title?
+
+
+ {{ RoleTitleMap[val] }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/tour/Slideshow.vue b/packages/frontend-2/components/tour/Slideshow.vue
new file mode 100644
index 000000000..167a01d6e
--- /dev/null
+++ b/packages/frontend-2/components/tour/Slideshow.vue
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+ Skip
+
+
+ Resume Tour
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/tour/content/BasicViewerNavigation.vue b/packages/frontend-2/components/tour/content/BasicViewerNavigation.vue
new file mode 100644
index 000000000..5c0a5f5e6
--- /dev/null
+++ b/packages/frontend-2/components/tour/content/BasicViewerNavigation.vue
@@ -0,0 +1,39 @@
+
+
+
+ You can easily navigate it by
+ rotating
+ (left mouse button),
+ zooming
+ (scroll) and
+ panning
+ (right mouse button). Give it a try now!
+
+
+
+ {{ encouragements[controlEndCounts] }}
+
+
+
+
diff --git a/packages/frontend-2/components/tour/content/FirstTip.vue b/packages/frontend-2/components/tour/content/FirstTip.vue
new file mode 100644
index 000000000..9e9a67cfa
--- /dev/null
+++ b/packages/frontend-2/components/tour/content/FirstTip.vue
@@ -0,0 +1,12 @@
+
+
+
+ Let's run through a few fast tips! This is Speckle's 3D viewer, and what you're
+ looking at is a
+ model.
+
+
+ Next, we're going to learn how to navigate it!
+
+
+
diff --git a/packages/frontend-2/components/tour/content/LastTip.vue b/packages/frontend-2/components/tour/content/LastTip.vue
new file mode 100644
index 000000000..8b9d5a9ab
--- /dev/null
+++ b/packages/frontend-2/components/tour/content/LastTip.vue
@@ -0,0 +1,12 @@
+
+
+
There's much more to Speckle.
+
+
+
diff --git a/packages/frontend-2/components/tour/content/OverlayModel.vue b/packages/frontend-2/components/tour/content/OverlayModel.vue
new file mode 100644
index 000000000..04928755d
--- /dev/null
+++ b/packages/frontend-2/components/tour/content/OverlayModel.vue
@@ -0,0 +1,64 @@
+
+
+
+
+ Speckle allows you to load multiple models in the same viewer.
+
+
+ Click here
+
+ to give it a try!
+
+
+
+
+
+ Nice - you've just created a "federated" model. Ready for what's next?
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/user/Avatar.vue b/packages/frontend-2/components/user/Avatar.vue
new file mode 100644
index 000000000..e58e47d77
--- /dev/null
+++ b/packages/frontend-2/components/user/Avatar.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+ {{ initials }}
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/user/avatar/Editable.vue b/packages/frontend-2/components/user/avatar/Editable.vue
new file mode 100644
index 000000000..ecafa4f16
--- /dev/null
+++ b/packages/frontend-2/components/user/avatar/Editable.vue
@@ -0,0 +1,36 @@
+
+
+
+
diff --git a/packages/frontend-2/components/user/avatar/Editor.vue b/packages/frontend-2/components/user/avatar/Editor.vue
new file mode 100644
index 000000000..38fc052f3
--- /dev/null
+++ b/packages/frontend-2/components/user/avatar/Editor.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Click here or drag and drop an image
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/user/avatar/Group.vue b/packages/frontend-2/components/user/avatar/Group.vue
new file mode 100644
index 000000000..2b2d01037
--- /dev/null
+++ b/packages/frontend-2/components/user/avatar/Group.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+ +{{ finalHiddenItemCount }}
+
+
+
+
diff --git a/packages/frontend-2/components/user/profile/EditDialog.vue b/packages/frontend-2/components/user/profile/EditDialog.vue
new file mode 100644
index 000000000..84e74755e
--- /dev/null
+++ b/packages/frontend-2/components/user/profile/EditDialog.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/user/profile/EditDialog/Bio.vue b/packages/frontend-2/components/user/profile/EditDialog/Bio.vue
new file mode 100644
index 000000000..042e3b4ee
--- /dev/null
+++ b/packages/frontend-2/components/user/profile/EditDialog/Bio.vue
@@ -0,0 +1,98 @@
+
+
+
+
+ Bio
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/user/profile/EditDialog/ChangePassword.vue b/packages/frontend-2/components/user/profile/EditDialog/ChangePassword.vue
new file mode 100644
index 000000000..83cb6668d
--- /dev/null
+++ b/packages/frontend-2/components/user/profile/EditDialog/ChangePassword.vue
@@ -0,0 +1,27 @@
+
+
+
+
+ Press the button below to start the password reset process. Once pressed, you
+ will receive an e-mail with further instructions.
+
+
+ Reset password
+
+
+
+
+
diff --git a/packages/frontend-2/components/user/profile/EditDialog/DeleteAccount.vue b/packages/frontend-2/components/user/profile/EditDialog/DeleteAccount.vue
new file mode 100644
index 000000000..5903b71c1
--- /dev/null
+++ b/packages/frontend-2/components/user/profile/EditDialog/DeleteAccount.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/user/profile/EditDialog/NotificationPreferences.vue b/packages/frontend-2/components/user/profile/EditDialog/NotificationPreferences.vue
new file mode 100644
index 000000000..cdb8c8507
--- /dev/null
+++ b/packages/frontend-2/components/user/profile/EditDialog/NotificationPreferences.vue
@@ -0,0 +1,89 @@
+
+
+
+
+ Notification preferences
+
+
+
+
+ | Notification type |
+
+ {{ capitalize(channel) }}
+ |
+
+
+
+
+ |
+ {{ notificationTypeMapping[type] || 'Unknown' }}
+ |
+
+ onUpdate({ value: !!$event, type, channel })
+ "
+ />
+ |
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/AnchoredPoints.vue b/packages/frontend-2/components/viewer/AnchoredPoints.vue
new file mode 100644
index 000000000..272586901
--- /dev/null
+++ b/packages/frontend-2/components/viewer/AnchoredPoints.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
openNextThread(model)"
+ @prev="(model) => openPrevThread(model)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(spotlightUserId = null)"
+ >
+ Stop Following {{ spotlightUser?.userName.split(' ')[0] }}
+
+
+ Followed by {{ followers[0].name.split(' ')[0] }}
+
+ & {{ followers.length - 1 }}
+ {{ followers.length - 1 === 1 ? 'other' : 'others' }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/Base.vue b/packages/frontend-2/components/viewer/Base.vue
new file mode 100644
index 000000000..c3002ba9a
--- /dev/null
+++ b/packages/frontend-2/components/viewer/Base.vue
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/Controls.vue b/packages/frontend-2/components/viewer/Controls.vue
new file mode 100644
index 000000000..11974c4b1
--- /dev/null
+++ b/packages/frontend-2/components/viewer/Controls.vue
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/GlobalFilterReset.vue b/packages/frontend-2/components/viewer/GlobalFilterReset.vue
new file mode 100644
index 000000000..38189c8a5
--- /dev/null
+++ b/packages/frontend-2/components/viewer/GlobalFilterReset.vue
@@ -0,0 +1,26 @@
+
+
+
+
+ Reset Filters
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/LoadingBar.vue b/packages/frontend-2/components/viewer/LoadingBar.vue
new file mode 100644
index 000000000..ac3c8cc4f
--- /dev/null
+++ b/packages/frontend-2/components/viewer/LoadingBar.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/PostSetupWrapper.vue b/packages/frontend-2/components/viewer/PostSetupWrapper.vue
new file mode 100644
index 000000000..6f5ade7bd
--- /dev/null
+++ b/packages/frontend-2/components/viewer/PostSetupWrapper.vue
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/Scope.vue b/packages/frontend-2/components/viewer/Scope.vue
new file mode 100644
index 000000000..f9ac85e68
--- /dev/null
+++ b/packages/frontend-2/components/viewer/Scope.vue
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/anchored-point/NewThread.vue b/packages/frontend-2/components/viewer/anchored-point/NewThread.vue
new file mode 100644
index 000000000..8af0954c4
--- /dev/null
+++ b/packages/frontend-2/components/viewer/anchored-point/NewThread.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
onSubmit()"
+ @update:model-value="onInputUpdated"
+ />
+
+
+
+
+ onSubmit()"
+ />
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/anchored-point/Thread.vue b/packages/frontend-2/components/viewer/anchored-point/Thread.vue
new file mode 100644
index 000000000..d52b8b96a
--- /dev/null
+++ b/packages/frontend-2/components/viewer/anchored-point/Thread.vue
@@ -0,0 +1,552 @@
+
+
+
+
+
+
+
+
+ {{ isTypingMessage }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/anchored-point/User.vue b/packages/frontend-2/components/viewer/anchored-point/User.vue
new file mode 100644
index 000000000..c8efbb665
--- /dev/null
+++ b/packages/frontend-2/components/viewer/anchored-point/User.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/anchored-point/thread/Comment.vue b/packages/frontend-2/components/viewer/anchored-point/thread/Comment.vue
new file mode 100644
index 000000000..4f5bb3fd0
--- /dev/null
+++ b/packages/frontend-2/components/viewer/anchored-point/thread/Comment.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+ {{ comment.author.name }}
+
+ {{ timeFromNow }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/anchored-point/thread/CommentAttachments.vue b/packages/frontend-2/components/viewer/anchored-point/thread/CommentAttachments.vue
new file mode 100644
index 000000000..bb4f20b53
--- /dev/null
+++ b/packages/frontend-2/components/viewer/anchored-point/thread/CommentAttachments.vue
@@ -0,0 +1,175 @@
+
+
+
onAttachmentClick(attachment)"
+ >
+ {{ attachment.fileName }}
+
+
+
+
+
+
+ {{ dialogAttachment.fileName }}
+
+
+ {{
+ dialogAttachment.fileSize
+ ? prettyFileSize(dialogAttachment.fileSize)
+ : 'Download'
+ }}
+
+
+
+
+
+
+ Failed to preview attachment
+
+
+
+
+
+
+
+
+
+ Be cautious when downloading! Attachments are not scanned for harmful
+ content.
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/anchored-point/thread/NewReply.vue b/packages/frontend-2/components/viewer/anchored-point/thread/NewReply.vue
new file mode 100644
index 000000000..fe4f38e92
--- /dev/null
+++ b/packages/frontend-2/components/viewer/anchored-point/thread/NewReply.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/comments/Comments.vue b/packages/frontend-2/components/viewer/comments/Comments.vue
new file mode 100644
index 000000000..9c04870db
--- /dev/null
+++ b/packages/frontend-2/components/viewer/comments/Comments.vue
@@ -0,0 +1,129 @@
+
+
+
+
+ Discussion Visibility Options
+
+
+
+
+
+
+ Show In 3D Model
+
+
+
+
+ {{ includeArchived ? 'Hide' : 'Show' }} Resolved ({{
+ commentThreadsMetadata?.totalArchivedCount
+ }})
+
+
+
+
+ Exclude Other Versions
+
+
+
+
+
TODO: Empty state
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/comments/Editor.vue b/packages/frontend-2/components/viewer/comments/Editor.vue
new file mode 100644
index 000000000..c18f31563
--- /dev/null
+++ b/packages/frontend-2/components/viewer/comments/Editor.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/comments/ListItem.vue b/packages/frontend-2/components/viewer/comments/ListItem.vue
new file mode 100644
index 000000000..ac93a619c
--- /dev/null
+++ b/packages/frontend-2/components/viewer/comments/ListItem.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+ {{ thread.author.name }}
+
+ & {{ thread.replyAuthors.totalCount }} others
+
+
+
+
+ {{ thread.rawText }}
+
+
+
+
+
+
+ {{ thread.replies.totalCount }}
+ {{ thread.replies.totalCount === 1 ? 'reply' : 'replies' }}
+
+
+ {{ formattedDate }}
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/controls/ButtonGroup.vue b/packages/frontend-2/components/viewer/controls/ButtonGroup.vue
new file mode 100644
index 000000000..2852143b4
--- /dev/null
+++ b/packages/frontend-2/components/viewer/controls/ButtonGroup.vue
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/controls/ButtonToggle.vue b/packages/frontend-2/components/viewer/controls/ButtonToggle.vue
new file mode 100644
index 000000000..712435b45
--- /dev/null
+++ b/packages/frontend-2/components/viewer/controls/ButtonToggle.vue
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/explode/Menu.vue b/packages/frontend-2/components/viewer/explode/Menu.vue
new file mode 100644
index 000000000..72a10c020
--- /dev/null
+++ b/packages/frontend-2/components/viewer/explode/Menu.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+ 💥
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/explorer/Explorer.vue b/packages/frontend-2/components/viewer/explorer/Explorer.vue
new file mode 100644
index 000000000..2feaf7baf
--- /dev/null
+++ b/packages/frontend-2/components/viewer/explorer/Explorer.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+ Unfold
+
+
+ Collapse
+
+
+
+
+ (manualExpandLevel < e ? (manualExpandLevel = e) : '')"
+ />
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/explorer/Filters.vue b/packages/frontend-2/components/viewer/explorer/Filters.vue
new file mode 100644
index 000000000..33b68a922
--- /dev/null
+++ b/packages/frontend-2/components/viewer/explorer/Filters.vue
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+ {{ title.split('.').reverse()[0] || title || 'No Title' }}
+
+
+ Reset
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View More ({{ relevantFiltersSearched.length - itemCount }})
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/explorer/NavbarLink.vue b/packages/frontend-2/components/viewer/explorer/NavbarLink.vue
new file mode 100644
index 000000000..1dc1a534a
--- /dev/null
+++ b/packages/frontend-2/components/viewer/explorer/NavbarLink.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/explorer/NumericFilter.vue b/packages/frontend-2/components/viewer/explorer/NumericFilter.vue
new file mode 100644
index 000000000..be1cc221d
--- /dev/null
+++ b/packages/frontend-2/components/viewer/explorer/NumericFilter.vue
@@ -0,0 +1,78 @@
+
+
+
+
Range:
+
[{{ props.filter.min.toFixed(2) }},
+
{{ props.filter.max.toFixed(2) }}]
+
+
+
+
+
+
+
+
+ {{ roundedValues.min }}
+
+
+
+
+
+
+
+
+ {{ roundedValues.max }}
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/explorer/StringFilter.vue b/packages/frontend-2/components/viewer/explorer/StringFilter.vue
new file mode 100644
index 000000000..ea6432ef0
--- /dev/null
+++ b/packages/frontend-2/components/viewer/explorer/StringFilter.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+ View More ({{ filter.valueGroups.length - itemCount }})
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/explorer/StringFilterItem.vue b/packages/frontend-2/components/viewer/explorer/StringFilterItem.vue
new file mode 100644
index 000000000..4086d44f6
--- /dev/null
+++ b/packages/frontend-2/components/viewer/explorer/StringFilterItem.vue
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+ {{ item.value.split('.').reverse()[0] || item.value || 'No Name' }}
+
+ ({{ availableTargetIds.length }})
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/explorer/TreeItem.vue b/packages/frontend-2/components/viewer/explorer/TreeItem.vue
new file mode 100644
index 000000000..8c711593f
--- /dev/null
+++ b/packages/frontend-2/components/viewer/explorer/TreeItem.vue
@@ -0,0 +1,363 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
setSelection(e)"
+ >
+
+
+
+ {{ header || headerAndSubheader.header }}
+
+
+ {{ subHeader || headerAndSubheader.subheader }}
+
+
+ unfold: {{ unfold }} / selected: {{ isSelected }} / hidden:
+ {{ isHidden }} / isolated:
+ {{ isIsolated }}
+
+
+
+
+
+
+
+
+
+
+ ({{ childrenLength }})
+
+
+
+
+
+
+ single: {{ isSingleCollection }}; multiple: {{ isMultipleCollection }}; a:
+ {{ isAtomic }}
+
+
+
+
+
+
+
+
+
+ $emit('expanded', e)"
+ />
+
+
+
+
+
+
+ $emit('expanded', e)"
+ />
+
+
+
+ View More ({{ singleCollectionItems.length - itemCount }})
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/filters/Filters.vue b/packages/frontend-2/components/viewer/filters/Filters.vue
new file mode 100644
index 000000000..791b8034d
--- /dev/null
+++ b/packages/frontend-2/components/viewer/filters/Filters.vue
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/layout/Panel.vue b/packages/frontend-2/components/viewer/layout/Panel.vue
new file mode 100644
index 000000000..a8ed1ecbb
--- /dev/null
+++ b/packages/frontend-2/components/viewer/layout/Panel.vue
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/resources/List.vue b/packages/frontend-2/components/viewer/resources/List.vue
new file mode 100644
index 000000000..22b386396
--- /dev/null
+++ b/packages/frontend-2/components/viewer/resources/List.vue
@@ -0,0 +1,80 @@
+
+
+
+
+ Add
+
+
+ {{ showRemove ? 'Done' : 'Remove' }}
+
+
+
+
+
+ removeModel(id)"
+ />
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/resources/ModelCard.vue b/packages/frontend-2/components/viewer/resources/ModelCard.vue
new file mode 100644
index 000000000..c7e75f6ab
--- /dev/null
+++ b/packages/frontend-2/components/viewer/resources/ModelCard.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ modelName.header }}
+
+
+
+ {{ isLatest ? 'latest version' : timeAgoCreatedAt }}
+
+
+
+
+
{{ model.versions?.totalCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ showLoadMore ? 'View older versions' : 'No more versions' }}
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/resources/VersionCard.vue b/packages/frontend-2/components/viewer/resources/VersionCard.vue
new file mode 100644
index 000000000..65cddae9e
--- /dev/null
+++ b/packages/frontend-2/components/viewer/resources/VersionCard.vue
@@ -0,0 +1,100 @@
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/resources/add-model/Dialog.vue b/packages/frontend-2/components/viewer/resources/add-model/Dialog.vue
new file mode 100644
index 000000000..76a0e94c8
--- /dev/null
+++ b/packages/frontend-2/components/viewer/resources/add-model/Dialog.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/resources/add-model/DialogModelTab.vue b/packages/frontend-2/components/viewer/resources/add-model/DialogModelTab.vue
new file mode 100644
index 000000000..c6729a136
--- /dev/null
+++ b/packages/frontend-2/components/viewer/resources/add-model/DialogModelTab.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
(queryLoading = $event)"
+ @model-clicked="onModelClicked"
+ @clear-search="clear"
+ />
+
+
+
diff --git a/packages/frontend-2/components/viewer/resources/add-model/DialogObjectTab.vue b/packages/frontend-2/components/viewer/resources/add-model/DialogObjectTab.vue
new file mode 100644
index 000000000..6ce6c2ef7
--- /dev/null
+++ b/packages/frontend-2/components/viewer/resources/add-model/DialogObjectTab.vue
@@ -0,0 +1,86 @@
+
+
+
+ Add objects from the current project by their IDs or an Object URL.
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/selection/Object.vue b/packages/frontend-2/components/viewer/selection/Object.vue
new file mode 100644
index 000000000..5eae08485
--- /dev/null
+++ b/packages/frontend-2/components/viewer/selection/Object.vue
@@ -0,0 +1,190 @@
+
+
+
+
+
+
+
+
+
+ {{ kvp.key }}
+
+
+
+ {{ kvp.value === null ? 'null' : kvp.value }}
+
+
+
+
+
+
+
+
+
+ {{ kvp.key }}
+
+
+
{{ kvp.innerType }} array
+
({{ kvp.arrayLength }})
+
+
+
+
+
+
+ {{ kvp.key }}
+
+
+
{{ kvp.arrayPreview }}
+
({{ kvp.arrayLength }})
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/selection/Sidebar.vue b/packages/frontend-2/components/viewer/selection/Sidebar.vue
new file mode 100644
index 000000000..003c84136
--- /dev/null
+++ b/packages/frontend-2/components/viewer/selection/Sidebar.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View More ({{ objects.length - itemCount }})
+
+
+
+ Hold down "shift" to select multiple objects.
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/settings/Menu.vue b/packages/frontend-2/components/viewer/settings/Menu.vue
new file mode 100644
index 000000000..32b0e2243
--- /dev/null
+++ b/packages/frontend-2/components/viewer/settings/Menu.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+ Local Settings
+
+
+ Prevent camera from going under the model
+
+
+ {{ localViewerSettings.turntableMode ? 'ON' : 'OFF' }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/sun/Menu.vue b/packages/frontend-2/components/viewer/sun/Menu.vue
new file mode 100644
index 000000000..41de639ca
--- /dev/null
+++ b/packages/frontend-2/components/viewer/sun/Menu.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/components/viewer/views/Menu.vue b/packages/frontend-2/components/viewer/views/Menu.vue
new file mode 100644
index 000000000..48759b5c8
--- /dev/null
+++ b/packages/frontend-2/components/viewer/views/Menu.vue
@@ -0,0 +1,84 @@
+
+
+
+
diff --git a/packages/frontend-2/composables/states.ts b/packages/frontend-2/composables/states.ts
new file mode 100644
index 000000000..644d90cfd
--- /dev/null
+++ b/packages/frontend-2/composables/states.ts
@@ -0,0 +1,9 @@
+export const useTextInputGlobalFocus = () =>
+ useState('text-input-focus', () => false)
+
+export const useTourStageState = () =>
+ useState('global-ui-element-state', () => ({
+ showNavbar: true,
+ showViewerControls: true,
+ showTour: false
+ }))
diff --git a/packages/frontend-2/error.vue b/packages/frontend-2/error.vue
new file mode 100644
index 000000000..bcec2eda4
--- /dev/null
+++ b/packages/frontend-2/error.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
{{ error.statusCode || 500 }}
+
{{ capitalize(error.message || '') }}
+
+
Go Home
+
+
+
+
+
+
diff --git a/packages/frontend-2/layouts/default.vue b/packages/frontend-2/layouts/default.vue
new file mode 100644
index 000000000..9386a3a93
--- /dev/null
+++ b/packages/frontend-2/layouts/default.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/layouts/empty.vue b/packages/frontend-2/layouts/empty.vue
new file mode 100644
index 000000000..0cb8364b6
--- /dev/null
+++ b/packages/frontend-2/layouts/empty.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/layouts/fullscreen.vue b/packages/frontend-2/layouts/fullscreen.vue
new file mode 100644
index 000000000..ba4672f56
--- /dev/null
+++ b/packages/frontend-2/layouts/fullscreen.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/frontend-2/layouts/hydrationtest.vue b/packages/frontend-2/layouts/hydrationtest.vue
new file mode 100644
index 000000000..ba4672f56
--- /dev/null
+++ b/packages/frontend-2/layouts/hydrationtest.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/frontend-2/layouts/landing.vue b/packages/frontend-2/layouts/landing.vue
new file mode 100644
index 000000000..6c4315bef
--- /dev/null
+++ b/packages/frontend-2/layouts/landing.vue
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/layouts/loginOrRegister.vue b/packages/frontend-2/layouts/loginOrRegister.vue
new file mode 100644
index 000000000..876403145
--- /dev/null
+++ b/packages/frontend-2/layouts/loginOrRegister.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/layouts/onboarding.vue b/packages/frontend-2/layouts/onboarding.vue
new file mode 100644
index 000000000..fd0636f53
--- /dev/null
+++ b/packages/frontend-2/layouts/onboarding.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/layouts/viewer.vue b/packages/frontend-2/layouts/viewer.vue
new file mode 100644
index 000000000..e6cde5fa1
--- /dev/null
+++ b/packages/frontend-2/layouts/viewer.vue
@@ -0,0 +1,45 @@
+
+
+
+
+ nav
+
+
+ viewer ctrls
+
+
+ tour ctrls
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-2/lib/auth/composables/activeUser.ts b/packages/frontend-2/lib/auth/composables/activeUser.ts
new file mode 100644
index 000000000..cb21df4a5
--- /dev/null
+++ b/packages/frontend-2/lib/auth/composables/activeUser.ts
@@ -0,0 +1,49 @@
+import { useApolloClient, useQuery } from '@vue/apollo-composable'
+import { graphql } from '~~/lib/common/generated/gql'
+import md5 from '~~/lib/common/helpers/md5'
+
+export const activeUserQuery = graphql(`
+ query ActiveUserMainMetadata {
+ activeUser {
+ id
+ email
+ name
+ role
+ avatar
+ isOnboardingFinished
+ createdAt
+ verified
+ }
+ }
+`)
+
+/**
+ * Get active user.
+ * undefined - not yet resolved
+ * null - resolved that user is a guest
+ */
+export function useActiveUser() {
+ const { result, refetch, onResult } = useQuery(activeUserQuery)
+
+ const activeUser = computed(() =>
+ result.value ? result.value.activeUser : undefined
+ )
+ const isLoggedIn = computed(() => !!activeUser.value?.id)
+ const distinctId = computed(() => {
+ const user = activeUser.value
+ if (!user) return user // null or undefined
+ if (!user.email) return null
+
+ return '@' + md5(user.email.toLowerCase()).toUpperCase()
+ })
+
+ return { activeUser, isLoggedIn, distinctId, refetch, onResult }
+}
+
+/**
+ * Prevnets setup function from resolving until active user is resolved
+ */
+export async function useWaitForActiveUser() {
+ const client = useApolloClient().client
+ await client.query({ query: activeUserQuery }).catch(() => void 0)
+}
diff --git a/packages/frontend-2/lib/auth/composables/auth.ts b/packages/frontend-2/lib/auth/composables/auth.ts
new file mode 100644
index 000000000..6ea70ea61
--- /dev/null
+++ b/packages/frontend-2/lib/auth/composables/auth.ts
@@ -0,0 +1,333 @@
+import {
+ getAccessCode,
+ getTokenFromAccessCode,
+ registerAndGetAccessCode
+} from '~~/lib/auth/services/auth'
+import { ensureError, Optional, SafeLocalStorage } from '@speckle/shared'
+import { CookieKeys, LocalStorageKeys } from '~~/lib/common/helpers/constants'
+import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie'
+import { useNavigateToHome, useNavigateToLogin } from '~~/lib/common/helpers/route'
+import { useApolloClient } from '@vue/apollo-composable'
+import { speckleWebAppId } from '~~/lib/auth/helpers/strategies'
+import { randomString } from '~~/lib/common/helpers/random'
+import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
+import { useMixpanel, useMixpanelUserIdentification } from '~~/lib/core/composables/mp'
+import { useActiveUser } from '~~/lib/auth/composables/activeUser'
+import { usePostAuthRedirect } from '~~/lib/auth/composables/postAuthRedirect'
+
+/**
+ * TODO:
+ * - OAuth error page w/ message passed from server
+ * - Invite redirects from server
+ * - Verify overall flow - does this make sense (from a security perspective as well)?
+ * - Does challenge do anything?
+ * - Do we really need this back and forth of multiple requests for local auth?
+ * - Can we get rid of backend redirecting to / with access_code in querystring?
+ */
+
+/**
+ * Composable that builds a function for resetting the active auth state.
+ * This means resetting mixpanel identification, wiping apollo `me` cache etc.
+ */
+const useResetAuthState = () => {
+ const apollo = useApolloClient().client
+ const { reidentify } = useMixpanelUserIdentification()
+ const { refetch } = useActiveUser()
+
+ return async () => {
+ // evict cache
+ apollo.cache.evict({ id: 'ROOT_QUERY', fieldName: 'activeUser' })
+
+ // wait till active user is reloaded
+ await refetch()
+
+ // re-identify mixpanel user
+ reidentify()
+ }
+}
+
+export const useAuthCookie = () =>
+ useSynchronizedCookie>(CookieKeys.AuthToken, {
+ maxAge: 60 * 60 * 24 * 30 // 30 days
+ })
+
+export const useAuthManager = () => {
+ const {
+ public: { apiOrigin }
+ } = useRuntimeConfig()
+
+ const resetAuthState = useResetAuthState()
+ const route = useRoute()
+ const goHome = useNavigateToHome()
+ const goToLogin = useNavigateToLogin()
+ const { triggerNotification } = useGlobalToast()
+ const mixpanel = useMixpanel()
+ const postAuthRedirect = usePostAuthRedirect()
+
+ /**
+ * Invite token, if any
+ */
+ const inviteToken = computed(() => route.query.token as Optional)
+
+ /**
+ * Observable auth cookie
+ */
+ const authToken = useAuthCookie()
+
+ /**
+ * Set/clear new token value and redirect to home
+ */
+ const saveNewToken = async (
+ newToken?: string,
+ options?: Partial<{ skipRedirect: boolean }>
+ ) => {
+ // write to cookie
+ authToken.value = newToken
+
+ // reset challenge
+ SafeLocalStorage.remove(LocalStorageKeys.AuthAppChallenge)
+
+ // Wipe auth state
+ await resetAuthState()
+
+ // redirect home & wipe access code from querystring
+ if (!options?.skipRedirect) goHome({ query: {} })
+ }
+
+ /**
+ * Check for access_code in query string and attempt to finalize login
+ */
+ const finalizeLoginWithAccessCode = async (
+ options?: Partial<{ skipRedirect: boolean }>
+ ) => {
+ const accessCode = route.query['access_code'] as Optional
+ const challenge = SafeLocalStorage.get(LocalStorageKeys.AuthAppChallenge) || ''
+ if (!accessCode) return
+
+ try {
+ const newToken = await getTokenFromAccessCode({
+ accessCode,
+ challenge,
+ apiOrigin
+ })
+
+ await saveNewToken(newToken, options)
+ } catch (error) {
+ await saveNewToken(undefined)
+ throw error
+ }
+ }
+
+ /**
+ * Check for 'emailverifiedstatus' in query string and report it to user
+ */
+ const watchEmailVerificationStatus = () => {
+ if (process.server) return
+
+ watch(
+ () =>
+ [
+ route.query['emailverifiedstatus'] as Optional,
+ route.query['emailverifiederror'] as Optional
+ ],
+ (newVals, oldVals) => {
+ const [newStatus, newError] = newVals
+ const [oldStatus, oldError] = oldVals || []
+
+ const isNewStatus = newStatus && newStatus !== oldStatus
+ const isNewError = newError && newError !== oldError
+
+ if (isNewStatus && newStatus === 'true') {
+ triggerNotification({
+ type: ToastNotificationType.Success,
+ title: 'Email successfully verified!'
+ })
+
+ // wipe report
+ goHome({ query: {} })
+ } else if (isNewError) {
+ triggerNotification({
+ type: ToastNotificationType.Danger,
+ title: 'Email verification failed',
+ description: newError
+ })
+
+ // wipe report
+ goHome({ query: {} })
+ }
+ },
+ { immediate: true }
+ )
+ }
+
+ /**
+ * Watch for changes to query string and when access_code is set trigger login finalization
+ * (either used just logged in or registered)
+ */
+ const watchLoginAccessCode = () => {
+ if (process.server) return
+
+ watch(
+ () => route.query['access_code'] as Optional,
+ async (newVal, oldVal) => {
+ if (newVal && newVal !== oldVal) {
+ try {
+ await finalizeLoginWithAccessCode({
+ skipRedirect: postAuthRedirect.hadPendingRedirect.value
+ })
+
+ triggerNotification({
+ type: ToastNotificationType.Success,
+ title: 'Welcome!',
+ description: "You've been successfully authenticated"
+ })
+
+ postAuthRedirect.popAndFollowRedirect()
+ } catch (e) {
+ triggerNotification({
+ type: ToastNotificationType.Danger,
+ title: 'Authentication failed',
+ description: `${ensureError(e).message}`
+ })
+ }
+ }
+ },
+ { immediate: true }
+ )
+ }
+
+ /**
+ * Sets up querystring watchers that trigger various auth related activities like email verification status reports etc.
+ */
+ const watchAuthQueryString = () => {
+ watchLoginAccessCode()
+ watchEmailVerificationStatus()
+ }
+
+ /**
+ * Trigger login through email & password
+ */
+ const loginWithEmail = async (params: {
+ email: string
+ password: string
+ challenge: string
+ }) => {
+ const { email, password, challenge } = params
+
+ const { accessCode } = await getAccessCode({
+ email,
+ password,
+ apiOrigin,
+ challenge
+ })
+
+ // eslint-disable-next-line camelcase
+ goHome({ query: { access_code: accessCode } })
+
+ mixpanel.track('Log In', { type: 'action' })
+ }
+
+ /**
+ * Trigger registration procedure with email & password
+ */
+ const signUpWithEmail = async (params: {
+ user: {
+ email: string
+ password: string
+ name: string
+ company?: string
+ }
+ challenge: string
+ inviteToken?: string
+ }) => {
+ const { user, challenge, inviteToken } = params
+
+ const { accessCode } = await registerAndGetAccessCode({
+ apiOrigin,
+ challenge,
+ user,
+ inviteToken
+ })
+
+ // eslint-disable-next-line camelcase
+ goHome({ query: { access_code: accessCode } })
+
+ mixpanel.track('Sign Up', {
+ type: 'action',
+ isInvite: !!inviteToken
+ })
+ }
+
+ /**
+ * Log out
+ */
+ const logout = async (
+ options?: Partial<{
+ skipToast: boolean
+ }>
+ ) => {
+ await saveNewToken(undefined, { skipRedirect: true })
+
+ if (!options?.skipToast) {
+ triggerNotification({
+ type: ToastNotificationType.Info,
+ title: 'Goodbye!',
+ description: "You've been logged out"
+ })
+ }
+
+ postAuthRedirect.deleteState()
+ goToLogin()
+ }
+
+ return {
+ authToken,
+ loginWithEmail,
+ signUpWithEmail,
+ logout,
+ watchAuthQueryString,
+ inviteToken
+ }
+}
+
+const useAuthAppIdAndChallenge = () => {
+ const route = useRoute()
+ const appId = ref('')
+ const challenge = ref('')
+
+ onMounted(() => {
+ // Resolve challenge & appId from querystring or generate them
+ const queryChallenge =
+ (route.query.challenge as Optional) ||
+ SafeLocalStorage.get(LocalStorageKeys.AuthAppChallenge)
+ const queryAppId = route.query.appId as Optional
+
+ appId.value = queryAppId || speckleWebAppId
+
+ if (queryChallenge) {
+ challenge.value = queryChallenge
+ } else if (appId.value === speckleWebAppId) {
+ const newChallenge = randomString(10)
+
+ SafeLocalStorage.set(LocalStorageKeys.AuthAppChallenge, newChallenge)
+ challenge.value = newChallenge
+ }
+ })
+
+ return { appId, challenge }
+}
+
+export const useLoginOrRegisterUtils = () => {
+ const appIdAndChallenge = useAuthAppIdAndChallenge()
+ const route = useRoute()
+
+ /**
+ * Invite token, if any
+ */
+ const inviteToken = computed(() => route.query.token as Optional)
+
+ return {
+ ...appIdAndChallenge,
+ inviteToken
+ }
+}
diff --git a/packages/frontend-2/lib/auth/composables/onboarding.ts b/packages/frontend-2/lib/auth/composables/onboarding.ts
new file mode 100644
index 000000000..198e0828f
--- /dev/null
+++ b/packages/frontend-2/lib/auth/composables/onboarding.ts
@@ -0,0 +1,168 @@
+import { useApolloClient } from '@vue/apollo-composable'
+import { useMixpanel } from '~~/lib/core/composables/mp'
+import { UnsupportedEnvironmentError } from '~~/lib/core/errors/base'
+import { OnboardingState } from '../helpers/onboarding'
+import { useActiveUser } from '~~/lib/auth/composables/activeUser'
+import { OnboardingError } from '~~/lib/auth/errors/errors'
+import { finishOnboardingMutation } from '~~/lib/auth/graphql/mutations'
+import {
+ convertThrowIntoFetchResult,
+ getFirstErrorMessage,
+ updateCacheByFilter
+} from '~~/lib/common/helpers/graphql'
+import { graphql } from '~~/lib/common/generated/gql'
+import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
+import { useNavigateToHome } from '~~/lib/common/helpers/route'
+import { projectsDashboardQuery } from '~~/lib/projects/graphql/queries'
+
+const ONBOARDING_PROP_INDUSTRY = 'onboarding_v1_industry'
+const ONBOARDING_PROP_ROLE = 'onboarding_v1_role'
+
+export function useProcessOnboarding() {
+ const mixpanel = useMixpanel()
+ const { distinctId, activeUser } = useActiveUser()
+ const apollo = useApolloClient().client
+ const { triggerNotification } = useGlobalToast()
+ const goHome = useNavigateToHome()
+
+ /**
+ * Sends to mp the segmentation info (industry, role)
+ * @param state
+ */
+ const setMixpanelSegments = (state: OnboardingState) => {
+ mixpanel.people.set_once(ONBOARDING_PROP_INDUSTRY, state.industry || null)
+ mixpanel.people.set_once(ONBOARDING_PROP_ROLE, state.role || null)
+ }
+
+ /**
+ * Clones the server-specified onboarding project for the (newly) logged in user.
+ */
+ const createOnboardingProject = async () => {
+ const createOnboardingProjectMutation = graphql(`
+ mutation CreateOnboardingProject {
+ projectMutations {
+ createForOnboarding {
+ ...ProjectPageProject
+ ...ProjectDashboardItem
+ }
+ }
+ }
+ `)
+ const { data } = await apollo
+ .mutate({
+ mutation: createOnboardingProjectMutation,
+ update: (cache, { data }) => {
+ if (!data?.projectMutations.createForOnboarding.id) return
+
+ const newProjectData = data.projectMutations.createForOnboarding
+
+ // Update User.projects
+ updateCacheByFilter(
+ cache,
+ { query: { query: projectsDashboardQuery } },
+ (cacheData) => {
+ if (!cacheData.activeUser?.projects) return
+ const newItems = [...cacheData.activeUser.projects.items, newProjectData]
+ return {
+ ...cacheData,
+ activeUser: {
+ ...cacheData.activeUser,
+ projects: {
+ ...cacheData.activeUser.projects,
+ items: newItems,
+ totalCount: (cacheData.activeUser.projects.totalCount || 0) + 1
+ }
+ }
+ }
+ }
+ )
+ }
+ })
+ .catch(convertThrowIntoFetchResult)
+
+ const newId = data?.projectMutations.createForOnboarding.id
+ return {
+ projectId: newId,
+ modelId: data?.projectMutations.createForOnboarding.models.items[0]?.id // TODO: less hardcoding maybe?
+ }
+ }
+
+ /**
+ * Marks the current user as having completed the onboarding - we're using this as a flag to
+ * know that we've set up the sample project once.
+ */
+ const setUserOnboardingComplete = async () => {
+ const user = activeUser.value
+ if (!user) throw new OnboardingError('Attempting to onboard unidentified user')
+ await apollo
+ .mutate({
+ mutation: finishOnboardingMutation,
+ update: (cache, { data }) => {
+ if (!data?.activeUserMutations.finishOnboarding) return
+
+ // Mark onboarding as finished in the cache
+ cache.modify({
+ id: cache.identify(user),
+ fields: {
+ isOnboardingFinished: () => true
+ }
+ })
+ }
+ })
+ .catch(convertThrowIntoFetchResult)
+ }
+
+ /**
+ * DEPRECATED
+ * @param state
+ * @param goToDashboard
+ */
+ const finishOnboarding = async (state: OnboardingState, goToDashboard = true) => {
+ const user = activeUser.value
+
+ if (process.server)
+ throw new UnsupportedEnvironmentError("Can't process onboarding during SSR")
+
+ if (!distinctId.value || !user)
+ throw new OnboardingError('Attempting to onboard unidentified user')
+
+ // Send data to mixpanel
+ mixpanel.people.set_once(ONBOARDING_PROP_INDUSTRY, state.industry || null)
+ mixpanel.people.set_once(ONBOARDING_PROP_ROLE, state.role || null)
+
+ // Mark onboarding as finished
+ const { data, errors } = await apollo
+ .mutate({
+ mutation: finishOnboardingMutation,
+ update: (cache, { data }) => {
+ if (!data?.activeUserMutations.finishOnboarding) return
+
+ // Mark onboarding as finished in the cache
+ cache.modify({
+ id: cache.identify(user),
+ fields: {
+ isOnboardingFinished: () => true
+ }
+ })
+ }
+ })
+ .catch(convertThrowIntoFetchResult)
+
+ if (data?.activeUserMutations.finishOnboarding) {
+ if (goToDashboard) goHome()
+ } else {
+ const errMsg = getFirstErrorMessage(errors)
+ triggerNotification({
+ type: ToastNotificationType.Danger,
+ title: errMsg
+ })
+ }
+ }
+
+ return {
+ setMixpanelSegments,
+ createOnboardingProject,
+ setUserOnboardingComplete,
+ finishOnboarding
+ }
+}
diff --git a/packages/frontend-2/lib/auth/composables/passwordReset.ts b/packages/frontend-2/lib/auth/composables/passwordReset.ts
new file mode 100644
index 000000000..828d2e24a
--- /dev/null
+++ b/packages/frontend-2/lib/auth/composables/passwordReset.ts
@@ -0,0 +1,60 @@
+import { ensureError } from '@speckle/shared'
+import {
+ requestResetEmail,
+ finalizePasswordReset
+} from '~~/lib/auth/services/resetPassword'
+import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
+import { useNavigateToLogin } from '~~/lib/common/helpers/route'
+
+export function usePasswordReset() {
+ const {
+ public: { apiOrigin }
+ } = useRuntimeConfig()
+ const { triggerNotification } = useGlobalToast()
+ const goToLogin = useNavigateToLogin()
+
+ const loading = ref(false)
+
+ const sendResetEmail = async (email: string) => {
+ try {
+ loading.value = true
+ await requestResetEmail({ email, apiOrigin })
+ triggerNotification({
+ type: ToastNotificationType.Info,
+ title: 'Password reset process initialized',
+ description: `We've sent you instructions on how to reset your password at ${email}`
+ })
+ } catch (e) {
+ triggerNotification({
+ type: ToastNotificationType.Danger,
+ title: 'Password reset failed',
+ description: `${ensureError(e).message}`
+ })
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const finalize = async (password: string, token: string) => {
+ try {
+ loading.value = true
+ await finalizePasswordReset({ password, token, apiOrigin })
+ triggerNotification({
+ type: ToastNotificationType.Success,
+ title: 'Password successfully changed',
+ description: `You can now log in with your new password`
+ })
+ goToLogin()
+ } catch (e) {
+ triggerNotification({
+ type: ToastNotificationType.Danger,
+ title: 'Password change failed',
+ description: `${ensureError(e).message}`
+ })
+ } finally {
+ loading.value = false
+ }
+ }
+
+ return { sendResetEmail, finalize }
+}
diff --git a/packages/frontend-2/lib/auth/composables/postAuthRedirect.ts b/packages/frontend-2/lib/auth/composables/postAuthRedirect.ts
new file mode 100644
index 000000000..81de2bd39
--- /dev/null
+++ b/packages/frontend-2/lib/auth/composables/postAuthRedirect.ts
@@ -0,0 +1,61 @@
+import { Optional } from '@speckle/shared'
+import { reduce } from 'lodash-es'
+import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie'
+import { CookieKeys } from '~~/lib/common/helpers/constants'
+
+const usePostAuthRedirectCookie = () =>
+ useSynchronizedCookie>(CookieKeys.PostAuthRedirect, {
+ maxAge: 60 * 5 // 5 mins
+ })
+
+export const usePostAuthRedirect = () => {
+ const cookie = usePostAuthRedirectCookie()
+ const router = useRouter()
+ const route = useRoute()
+
+ const hadPendingRedirect = computed(() => !!cookie.value?.length)
+
+ const deleteState = () => (cookie.value = undefined)
+ const set = (pathWithQuery: string, force?: boolean) => {
+ const currVal = cookie.value
+ if (currVal && !force) return
+ cookie.value = pathWithQuery
+ }
+ const setCurrentRoute = (force?: boolean) => {
+ set(route.fullPath, force)
+ }
+ const popAndFollowRedirect = () => {
+ const pathWithQuery = cookie.value
+ if (!pathWithQuery) return
+
+ deleteState()
+
+ if (process.server) {
+ const url = new URL(pathWithQuery, 'http://notimportant.com')
+ router
+ .push({
+ path: url.pathname,
+ query: reduce(
+ [...url.searchParams.entries()],
+ (result, entry) => {
+ result[entry[0]] = entry[1]
+ return result
+ },
+ {} as Record
+ )
+ })
+ .catch(console.error)
+ } else {
+ // cause nuxt doesn't show error page for some reason
+ window.location.href = pathWithQuery
+ }
+ }
+
+ return {
+ set,
+ deleteState,
+ popAndFollowRedirect,
+ setCurrentRoute,
+ hadPendingRedirect
+ }
+}
diff --git a/packages/frontend-2/lib/auth/errors/errors.ts b/packages/frontend-2/lib/auth/errors/errors.ts
new file mode 100644
index 000000000..6dde8ddc6
--- /dev/null
+++ b/packages/frontend-2/lib/auth/errors/errors.ts
@@ -0,0 +1,21 @@
+import { BaseError } from '~~/lib/core/errors/base'
+
+export class InvalidLoginParametersError extends BaseError {
+ static defaultMessage = 'Invalid parameters for logging in!'
+}
+
+export class AuthFailedError extends BaseError {
+ static defaultMessage = 'Logging in failed!'
+}
+
+export class InvalidRegisterParametersError extends BaseError {
+ static defaultMessage = 'Invalid parameters for signing up!'
+}
+
+export class PasswordResetError extends BaseError {
+ static defaultMessage = 'Something went wrong while resetting password'
+}
+
+export class OnboardingError extends BaseError {
+ static defaultMessage = 'Something went wrong while onboarding the user'
+}
diff --git a/packages/frontend-2/lib/auth/graphql/mutations.ts b/packages/frontend-2/lib/auth/graphql/mutations.ts
new file mode 100644
index 000000000..1037c8cfc
--- /dev/null
+++ b/packages/frontend-2/lib/auth/graphql/mutations.ts
@@ -0,0 +1,9 @@
+import { graphql } from '~~/lib/common/generated/gql'
+
+export const finishOnboardingMutation = graphql(`
+ mutation FinishOnboarding {
+ activeUserMutations {
+ finishOnboarding
+ }
+ }
+`)
diff --git a/packages/frontend-2/lib/auth/graphql/queries.ts b/packages/frontend-2/lib/auth/graphql/queries.ts
new file mode 100644
index 000000000..63520b587
--- /dev/null
+++ b/packages/frontend-2/lib/auth/graphql/queries.ts
@@ -0,0 +1,32 @@
+import { graphql } from '~~/lib/common/generated/gql'
+
+export const loginServerInfoQuery = graphql(`
+ query AuthServerInfo {
+ serverInfo {
+ ...AuthStategiesServerInfoFragment
+ ...ServerTermsOfServicePrivacyPolicyFragment
+ ...AuthRegisterPanelServerInfo
+ }
+ }
+`)
+
+export const authorizableAppMetadataQuery = graphql(`
+ query AuthorizableAppMetadata($id: String!) {
+ app(id: $id) {
+ id
+ name
+ description
+ trustByDefault
+ redirectUrl
+ scopes {
+ name
+ description
+ }
+ author {
+ name
+ id
+ avatar
+ }
+ }
+ }
+`)
diff --git a/packages/frontend-2/lib/auth/helpers/onboarding.ts b/packages/frontend-2/lib/auth/helpers/onboarding.ts
new file mode 100644
index 000000000..f0b5d7969
--- /dev/null
+++ b/packages/frontend-2/lib/auth/helpers/onboarding.ts
@@ -0,0 +1,31 @@
+export enum OnboardingIndustry {
+ Architecture = 'architecture',
+ Engineering = 'engineering',
+ Construction = 'construction',
+ Design = 'design',
+ Gaming = 'gaming',
+ Other = 'other'
+}
+
+export enum OnboardingRole {
+ BimManager = 'bim-manager',
+ ComputationalDesigner = 'computational-designer',
+ Architect = 'architect',
+ Engineer = 'engineer',
+ SoftwareDeveloper = 'software-developer',
+ Other = 'other'
+}
+
+export const RoleTitleMap: Record = {
+ [OnboardingRole.Architect]: 'Architect',
+ [OnboardingRole.BimManager]: 'BIM Manager',
+ [OnboardingRole.Engineer]: 'Engineer',
+ [OnboardingRole.SoftwareDeveloper]: 'Software Developer',
+ [OnboardingRole.ComputationalDesigner]: 'Computational Designer',
+ [OnboardingRole.Other]: 'Other'
+}
+
+export type OnboardingState = {
+ industry?: OnboardingIndustry
+ role?: OnboardingRole
+}
diff --git a/packages/frontend-2/lib/auth/helpers/strategies.ts b/packages/frontend-2/lib/auth/helpers/strategies.ts
new file mode 100644
index 000000000..b5d428a12
--- /dev/null
+++ b/packages/frontend-2/lib/auth/helpers/strategies.ts
@@ -0,0 +1,11 @@
+/**
+ * TODO: Does this need to change for new frontend?
+ */
+export const speckleWebAppId = 'spklwebapp'
+
+export enum AuthStrategy {
+ Local = 'local',
+ Google = 'google',
+ Github = 'github',
+ AzureAD = 'azuread'
+}
diff --git a/packages/frontend-2/lib/auth/helpers/validation.ts b/packages/frontend-2/lib/auth/helpers/validation.ts
new file mode 100644
index 000000000..9c877d077
--- /dev/null
+++ b/packages/frontend-2/lib/auth/helpers/validation.ts
@@ -0,0 +1,22 @@
+import { isStringOfLength, stringContains } from '~~/lib/common/helpers/validation'
+
+export const passwordLongEnough = isStringOfLength({ minLength: 8 })
+export const passwordHasAtLeastOneNumber = stringContains({
+ match: /\d/,
+ message: 'Must have at least one number'
+})
+export const passwordHasAtLeastOneLowercaseLetter = stringContains({
+ match: /[a-z]/,
+ message: 'Must have at least one lowercase letter'
+})
+export const passwordHasAtLeastOneUppercaseLetter = stringContains({
+ match: /[A-Z]/,
+ message: 'Must have at least one uppercase letter'
+})
+
+export const passwordRules = [
+ passwordLongEnough,
+ passwordHasAtLeastOneNumber,
+ passwordHasAtLeastOneLowercaseLetter,
+ passwordHasAtLeastOneUppercaseLetter
+]
diff --git a/packages/frontend-2/lib/auth/mocks/activeUser.ts b/packages/frontend-2/lib/auth/mocks/activeUser.ts
new file mode 100644
index 000000000..09bcacf2d
--- /dev/null
+++ b/packages/frontend-2/lib/auth/mocks/activeUser.ts
@@ -0,0 +1,85 @@
+import { Optional, Roles } from '@speckle/shared'
+import { Get } from 'type-fest'
+import { fakeUsers } from '~~/components/form/select/Users.stories'
+import {
+ ActiveUserMainMetadataQuery,
+ ActiveUserMainMetadataQueryVariables,
+ ProfileEditDialogQuery,
+ ProfileEditDialogQueryVariables
+} from '~~/lib/common/generated/gql/graphql'
+import { ApolloMockData } from '~~/lib/common/helpers/storybook'
+import { apolloMockRequestWithDefaults } from '~~/lib/fake-nuxt-env/utils/betterMockLink'
+
+type CombinedUserType = ApolloMockData<
+ NonNullable<
+ Get &
+ Get
+ >
+>
+
+const randomDate = new Date('01-01-2020')
+const randomLoggedInUser: CombinedUserType = {
+ __typename: 'User',
+ id: 'random-logged-in-user',
+ email: 'loggedin@user.com',
+ name: 'Logged in user',
+ role: Roles.Server.User,
+ avatar: fakeUsers[0].avatar,
+ isOnboardingFinished: false,
+ createdAt: randomDate.toISOString(),
+ verified: false,
+ company: 'Random company',
+ bio: 'Random bio',
+ notificationPreferences: {}
+}
+
+export const mockActiveUserQuery = apolloMockRequestWithDefaults<
+ ActiveUserMainMetadataQuery,
+ ActiveUserMainMetadataQueryVariables,
+ Optional>
+>({
+ request: ({ operationName }) => operationName === 'ActiveUserMainMetadata',
+ result: (_, values) => ({
+ data: {
+ __typename: 'Query',
+ activeUser: values?.forceGuest
+ ? null
+ : {
+ __typename: randomLoggedInUser.__typename,
+ id: randomLoggedInUser.id,
+ email: randomLoggedInUser.email,
+ name: randomLoggedInUser.name,
+ role: randomLoggedInUser.role,
+ avatar: randomLoggedInUser.avatar,
+ isOnboardingFinished: randomLoggedInUser.isOnboardingFinished,
+ createdAt: randomLoggedInUser.createdAt,
+ verified: randomLoggedInUser.verified
+ }
+ }
+ })
+})
+
+export const mockProfileEditDialogQuery = apolloMockRequestWithDefaults<
+ ProfileEditDialogQuery,
+ ProfileEditDialogQueryVariables,
+ Optional>
+>({
+ request: ({ operationName }) => operationName === 'ProfileEditDialog',
+ result: (_, values) => ({
+ data: {
+ __typename: 'Query',
+ activeUser: values?.forceGuest
+ ? null
+ : {
+ __typename: randomLoggedInUser.__typename,
+ id: randomLoggedInUser.id,
+ email: randomLoggedInUser.email,
+ name: randomLoggedInUser.name,
+ company: randomLoggedInUser.company,
+ avatar: randomLoggedInUser.avatar,
+ bio: randomLoggedInUser.bio,
+ notificationPreferences: randomLoggedInUser.notificationPreferences
+ }
+ }
+ })
+})
diff --git a/packages/frontend-2/lib/auth/mocks/serverInfo.ts b/packages/frontend-2/lib/auth/mocks/serverInfo.ts
new file mode 100644
index 000000000..98108aa4f
--- /dev/null
+++ b/packages/frontend-2/lib/auth/mocks/serverInfo.ts
@@ -0,0 +1,53 @@
+import {
+ apolloMockRequestWithDefaults,
+ MockedApolloFetchResult
+} from '~~/lib/fake-nuxt-env/utils/betterMockLink'
+import {
+ AuthServerInfoQuery,
+ AuthServerInfoQueryVariables
+} from '~~/lib/common/generated/gql/graphql'
+import { AuthStrategy } from '~~/lib/auth/helpers/strategies'
+
+export const mockLoginServerInfoQuery = apolloMockRequestWithDefaults<
+ AuthServerInfoQuery,
+ AuthServerInfoQueryVariables
+>({
+ request: ({ operationName }) => operationName === 'AuthServerInfo',
+ result: (): MockedApolloFetchResult => ({
+ data: {
+ __typename: 'Query',
+ serverInfo: {
+ __typename: 'ServerInfo',
+ authStrategies: [
+ {
+ id: AuthStrategy.Local,
+ name: 'Local',
+ url: '/',
+ __typename: 'AuthStrategy'
+ },
+ {
+ id: AuthStrategy.Google,
+ name: 'Google',
+ url: 'https://google.com',
+ __typename: 'AuthStrategy'
+ },
+ {
+ id: AuthStrategy.Github,
+ name: 'Github',
+ url: 'https://github.com',
+ __typename: 'AuthStrategy'
+ },
+ {
+ id: AuthStrategy.AzureAD,
+ name: 'Azure',
+ url: 'https://microsoft.com',
+ __typename: 'AuthStrategy'
+ }
+ ],
+ termsOfService:
+ 'This piece of text is managed by server admins! You agree to our Terms of Use and Privacy policy.',
+ inviteOnly: false
+ }
+ }
+ })
+})
diff --git a/packages/frontend-2/lib/auth/services/auth.ts b/packages/frontend-2/lib/auth/services/auth.ts
new file mode 100644
index 000000000..16db249c3
--- /dev/null
+++ b/packages/frontend-2/lib/auth/services/auth.ts
@@ -0,0 +1,137 @@
+import { isString } from 'lodash-es'
+import {
+ InvalidLoginParametersError,
+ AuthFailedError,
+ InvalidRegisterParametersError
+} from '~~/lib/auth/errors/errors'
+import { speckleWebAppId } from '~~/lib/auth/helpers/strategies'
+
+// TODO: Should these differ from the old frontend values?
+const appId = speckleWebAppId
+const appSecret = speckleWebAppId
+
+type LoginParams = {
+ apiOrigin: string
+ email: string
+ password: string
+ challenge: string
+}
+
+type TokenParams = {
+ accessCode: string
+ apiOrigin: string
+ challenge: string
+}
+
+type RegisterParams = {
+ apiOrigin: string
+ challenge: string
+ inviteToken?: string
+ user: {
+ email: string
+ password: string
+ name: string
+ company?: string
+ }
+}
+
+async function resolveAccessCode(res: Response): Promise {
+ if (!res.redirected) {
+ // for some reason the error response structure differs between /login and /register...
+ const body = (await res.json()) as { err?: boolean | string; message?: string }
+ if (body.err) {
+ const errMsg = isString(body.err)
+ ? body.err
+ : body.message || 'An issue occurred while resolving access code'
+ throw new AuthFailedError(errMsg)
+ }
+
+ throw new AuthFailedError('Authentication request unexpectedly did not redirect')
+ }
+
+ const redirectUrl = res.url
+ const accessCode = new URL(redirectUrl).searchParams.get('access_code')
+ if (!accessCode) {
+ throw new AuthFailedError('Unable to resolver access_code from auth response')
+ }
+
+ return accessCode
+}
+
+export async function getAccessCode(params: LoginParams) {
+ const { apiOrigin, email, password, challenge } = params
+
+ if (!email || !password) {
+ throw new InvalidLoginParametersError(
+ "Can't log in without a valid email and password!"
+ )
+ }
+
+ const loginUrl = new URL(
+ `/auth/local/login?challenge=${challenge}`,
+ apiOrigin
+ ).toString()
+
+ const res = await fetch(loginUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ redirect: 'follow',
+ body: JSON.stringify({ email, password })
+ })
+
+ return { accessCode: await resolveAccessCode(res) }
+}
+
+export async function registerAndGetAccessCode(params: RegisterParams) {
+ const { apiOrigin, challenge, user, inviteToken } = params
+ if (!user.email || !user.password || !user.name) {
+ throw new InvalidRegisterParametersError(
+ "Can't register without a valid email, password and name!"
+ )
+ }
+
+ const registerUrl = new URL(`/auth/local/register`, apiOrigin)
+ registerUrl.searchParams.append('challenge', challenge)
+ if (inviteToken) {
+ registerUrl.searchParams.append('token', inviteToken)
+ }
+
+ const res = await fetch(registerUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ redirect: 'follow',
+ body: JSON.stringify(user)
+ })
+
+ return { accessCode: await resolveAccessCode(res) }
+}
+
+export async function getTokenFromAccessCode(params: TokenParams) {
+ const { apiOrigin, accessCode, challenge } = params
+
+ const url = new URL('/auth/token', apiOrigin)
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ accessCode,
+ appId,
+ appSecret,
+ challenge
+ })
+ })
+
+ // TODO: Do we wanna start using refresh tokens?
+ const data = (await response.json()) as { token: string; refreshToken: string }
+ if (!data.token) {
+ throw new AuthFailedError("Couldn't resolve token through access code.")
+ }
+
+ return data.token
+}
diff --git a/packages/frontend-2/lib/auth/services/resetPassword.ts b/packages/frontend-2/lib/auth/services/resetPassword.ts
new file mode 100644
index 000000000..a92070956
--- /dev/null
+++ b/packages/frontend-2/lib/auth/services/resetPassword.ts
@@ -0,0 +1,44 @@
+import { PasswordResetError } from '~~/lib/auth/errors/errors'
+
+type RequestResetEmailParams = {
+ email: string
+ apiOrigin: string
+}
+
+type PasswordResetFinalizationParams = {
+ password: string
+ token: string
+ apiOrigin: string
+}
+
+export async function requestResetEmail(params: RequestResetEmailParams) {
+ const { email, apiOrigin } = params
+
+ const url = new URL('/auth/pwdreset/request', apiOrigin)
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email })
+ })
+
+ const body = await res.text()
+ if (res.status !== 200) {
+ throw new PasswordResetError(body)
+ }
+}
+
+export async function finalizePasswordReset(params: PasswordResetFinalizationParams) {
+ const { password, token, apiOrigin } = params
+
+ const url = new URL('/auth/pwdreset/finalize', apiOrigin)
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ tokenId: token, password })
+ })
+
+ const body = await res.text()
+ if (res.status !== 200) {
+ throw new PasswordResetError(body)
+ }
+}
diff --git a/packages/frontend-2/lib/common/composables/reactiveCookie.ts b/packages/frontend-2/lib/common/composables/reactiveCookie.ts
new file mode 100644
index 000000000..f9ba856b6
--- /dev/null
+++ b/packages/frontend-2/lib/common/composables/reactiveCookie.ts
@@ -0,0 +1,14 @@
+import { CookieOptions } from '#app'
+import { useScopedState } from '~~/lib/common/composables/scopedState'
+
+/**
+ * Makes useCookie() synchronized across the app so that a change to it from one place
+ * will also update other references elsewhere
+ */
+export const useSynchronizedCookie = (
+ name: string,
+ opts?: CookieOptions
+) =>
+ useScopedState(`synchronizedCookiesState-${name}`, () =>
+ useCookie(name, opts)
+ )
diff --git a/packages/frontend-2/lib/common/composables/scopedState.ts b/packages/frontend-2/lib/common/composables/scopedState.ts
new file mode 100644
index 000000000..70dee453f
--- /dev/null
+++ b/packages/frontend-2/lib/common/composables/scopedState.ts
@@ -0,0 +1,20 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+import { NuxtApp } from 'nuxt/dist/app/nuxt'
+
+/**
+ * Similar to nuxt's useState() except state is scoped to only the SSR request or only the client-side session.
+ * The state doesn't get serialized in SSR and thus won't be transferred to the client-side session
+ */
+export function useScopedState(key: string | symbol, init: () => T) {
+ const nuxtApp = useNuxtApp() as NuxtApp
+
+ if (!nuxtApp.__scopedStates) {
+ nuxtApp.__scopedStates = {}
+ }
+
+ if (!nuxtApp.__scopedStates[key]) {
+ nuxtApp.__scopedStates[key] = init()
+ }
+
+ return nuxtApp.__scopedStates[key] as T
+}
diff --git a/packages/frontend-2/lib/common/composables/serverInfo.ts b/packages/frontend-2/lib/common/composables/serverInfo.ts
new file mode 100644
index 000000000..124e7cdac
--- /dev/null
+++ b/packages/frontend-2/lib/common/composables/serverInfo.ts
@@ -0,0 +1,17 @@
+import { useQuery } from '@vue/apollo-composable'
+import { serverInfoBlobSizeLimitQuery } from '~~/lib/common/graphql/queries'
+import { prettyFileSize } from '~~/lib/core/helpers/file'
+
+export function useServerFileUploadLimit() {
+ const { result } = useQuery(serverInfoBlobSizeLimitQuery)
+
+ const maxSizeInBytes = computed(
+ () => result.value?.serverInfo.blobSizeLimitBytes || 0
+ )
+ const maxSizeDisplayString = computed(() => prettyFileSize(maxSizeInBytes.value))
+
+ return {
+ maxSizeInBytes,
+ maxSizeDisplayString
+ }
+}
diff --git a/packages/frontend-2/lib/common/composables/steps.ts b/packages/frontend-2/lib/common/composables/steps.ts
new file mode 100644
index 000000000..5305d6d5e
--- /dev/null
+++ b/packages/frontend-2/lib/common/composables/steps.ts
@@ -0,0 +1,102 @@
+import { ToRefs } from 'vue'
+import { HorizontalOrVertical, StepCoreType } from '~~/lib/common/helpers/components'
+import { clamp } from 'lodash-es'
+import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
+
+export function useStepsInternals(params: {
+ props: ToRefs<{
+ orientation?: HorizontalOrVertical
+ steps: StepCoreType[]
+ modelValue?: number
+ goVerticalBelow?: TailwindBreakpoints
+ nonInteractive?: boolean
+ }>
+ emit: {
+ (e: 'update:modelValue', val: number): void
+ }
+}) {
+ const {
+ props: { modelValue, steps, orientation, goVerticalBelow, nonInteractive },
+ emit
+ } = params
+
+ const finalOrientation = computed(
+ (): HorizontalOrVertical =>
+ orientation?.value === 'vertical' ? 'vertical' : 'horizontal'
+ )
+
+ const value = computed({
+ get: () => clamp(modelValue?.value || 0, -1, steps.value.length),
+ set: (newVal) => emit('update:modelValue', clamp(newVal, 0, steps.value.length))
+ })
+
+ const getStepDisplayValue = (step: number) => `${step + 1}`
+ const isCurrentStep = (step: number) => step === value.value
+ const isFinishedStep = (step: number) => step < value.value
+
+ const switchStep = (newStep: number, e?: MouseEvent) => {
+ if (nonInteractive?.value) {
+ e?.preventDefault()
+ e?.stopPropagation()
+ e?.stopImmediatePropagation()
+ return
+ }
+
+ value.value = newStep
+
+ const stepObj = steps.value[value.value]
+ stepObj?.onClick?.()
+ }
+
+ const listClasses = computed(() => {
+ const classParts: string[] = ['flex']
+
+ classParts.push('flex')
+ if (finalOrientation.value === 'vertical' || goVerticalBelow?.value) {
+ classParts.push('flex-col space-y-4 justify-center')
+
+ if (goVerticalBelow?.value === TailwindBreakpoints.sm) {
+ classParts.push(
+ 'sm:flex-row sm:space-y-0 sm:justify-start sm:space-x-8 sm:items-center'
+ )
+ } else if (goVerticalBelow?.value === TailwindBreakpoints.md) {
+ classParts.push(
+ 'md:flex-row md:space-y-0 md:justify-start md:space-x-8 md:items-center'
+ )
+ } else if (goVerticalBelow?.value === TailwindBreakpoints.lg) {
+ classParts.push(
+ 'lg:flex-row lg:space-y-0 lg:justify-start lg:space-x-8 lg:items-center'
+ )
+ } else if (goVerticalBelow?.value === TailwindBreakpoints.xl) {
+ classParts.push(
+ 'xl:flex-row xl:space-y-0 xl:justify-start xl:space-x-8 xl:items-center'
+ )
+ }
+ } else {
+ classParts.push('flex-row space-x-8 items-center')
+ }
+
+ return classParts.join(' ')
+ })
+
+ const linkClasses = computed(() => {
+ const classParts: string[] = ['flex items-center']
+
+ if (!nonInteractive?.value) {
+ classParts.push('cursor-pointer')
+ }
+
+ return classParts.join(' ')
+ })
+
+ return {
+ value,
+ isCurrentStep,
+ isFinishedStep,
+ switchStep,
+ getStepDisplayValue,
+ listClasses,
+ linkClasses,
+ orientation: finalOrientation
+ }
+}
diff --git a/packages/frontend-2/lib/common/composables/toast.ts b/packages/frontend-2/lib/common/composables/toast.ts
new file mode 100644
index 000000000..5c69b7272
--- /dev/null
+++ b/packages/frontend-2/lib/common/composables/toast.ts
@@ -0,0 +1,91 @@
+import { useTimeoutFn } from '@vueuse/core'
+import { Nullable } from '@speckle/shared'
+import { useScopedState } from '~/lib/common/composables/scopedState'
+import { Ref } from 'vue'
+
+export enum ToastNotificationType {
+ Success,
+ Warning,
+ Danger,
+ Info
+}
+
+export type ToastNotification = {
+ title?: string
+ /**
+ * Optionally provide extra text
+ */
+ description?: string
+ type: ToastNotificationType
+ /**
+ * Optionally specify a CTA link on the right
+ */
+ cta?: {
+ title: string
+ url?: string
+ onClick?: (e: MouseEvent) => void
+ }
+}
+
+const useGlobalToastState = () =>
+ useScopedState[>>('global-toast-state', () =>
+ ref(null)
+ )
+
+/**
+ * Set up a new global toast manager/renderer (don't use this in multiple components that live at the same time)
+ */
+export function useGlobalToastManager() {
+ const stateNotification = useGlobalToastState()
+
+ const currentNotification = ref(stateNotification.value)
+ const readOnlyNotification = computed(() => currentNotification.value)
+
+ const { start, stop } = useTimeoutFn(() => {
+ dismiss()
+ }, 4000)
+
+ watch(
+ stateNotification,
+ (newVal) => {
+ if (!newVal) return
+
+ // First dismiss old notification, then set a new one on next tick
+ // this is so that the old one actually disappears from the screen for the user,
+ // instead of just having its contents replaced
+ dismiss()
+
+ nextTick(() => {
+ currentNotification.value = newVal
+
+ // (re-)init timeout
+ stop()
+ start()
+ })
+ },
+ { deep: true }
+ )
+
+ const dismiss = () => {
+ currentNotification.value = null
+ stateNotification.value = null
+ }
+
+ return { currentNotification: readOnlyNotification, dismiss }
+}
+
+/**
+ * Trigger global toast notifications
+ */
+export function useGlobalToast() {
+ const stateNotification = useGlobalToastState()
+
+ /**
+ * Trigger a new toast notification
+ */
+ const triggerNotification = (notification: ToastNotification) => {
+ stateNotification.value = notification
+ }
+
+ return { triggerNotification }
+}
diff --git a/packages/frontend-2/lib/common/composables/url.ts b/packages/frontend-2/lib/common/composables/url.ts
new file mode 100644
index 000000000..1d6e8c750
--- /dev/null
+++ b/packages/frontend-2/lib/common/composables/url.ts
@@ -0,0 +1,50 @@
+import { reduce } from 'lodash-es'
+import { Nullable, Optional } from '@speckle/shared'
+
+export function serializeHashState(
+ state: Record>
+): Optional {
+ return !Object.values(state).filter((i) => i !== null).length
+ ? undefined
+ : `#${Object.entries(state)
+ .filter((entry): entry is [string, string] => !!entry[1])
+ .map(([key, val]) => `${key}=${val}`)
+ .join('&')}`
+}
+
+/**
+ * Read/writable state similar to one in the querystring, but one that uses anchor (#) data instead
+ */
+export function useRouteHashState() {
+ const route = useRoute()
+ const router = useRouter()
+
+ const hashState = computed({
+ get: () => {
+ const hash = route.hash
+ if (hash.length < 2 || !hash.startsWith('#')) return {}
+
+ const keyValuePairs = hash.substring(1).split('&')
+ return reduce(
+ keyValuePairs,
+ (result, item) => {
+ const [key, value] = item.split('=')
+ if (key && value) {
+ result[key] = value
+ }
+ return result
+ },
+ {} as Record>
+ )
+ },
+ set: (newVal) => {
+ const hashString = serializeHashState(newVal)
+ router.push({
+ query: route.query,
+ hash: hashString
+ })
+ }
+ })
+
+ return { hashState }
+}
diff --git a/packages/frontend-2/lib/common/composables/users.ts b/packages/frontend-2/lib/common/composables/users.ts
new file mode 100644
index 000000000..af21ae31d
--- /dev/null
+++ b/packages/frontend-2/lib/common/composables/users.ts
@@ -0,0 +1,20 @@
+import { useQuery } from '@vue/apollo-composable'
+import { UserSearchQueryVariables } from '~~/lib/common/generated/gql/graphql'
+import { userSearchQuery } from '~~/lib/common/graphql/queries'
+
+export function useUserSearch(params: { variables: Ref }) {
+ const { variables } = params
+ const { result, variables: usedVariables } = useQuery(
+ userSearchQuery,
+ variables,
+ () => ({
+ debounce: 300,
+ enabled: (variables.value.query || '').length >= 3
+ })
+ )
+
+ return {
+ userSearch: result,
+ searchVariables: usedVariables
+ }
+}
diff --git a/packages/frontend-2/lib/common/composables/window.ts b/packages/frontend-2/lib/common/composables/window.ts
new file mode 100644
index 000000000..388e3651f
--- /dev/null
+++ b/packages/frontend-2/lib/common/composables/window.ts
@@ -0,0 +1,105 @@
+import { MaybeRef } from '@vueuse/shared'
+import { debounce, isUndefined, throttle } from 'lodash-es'
+import { Nullable } from '@speckle/shared'
+
+export enum ThrottleOrDebounce {
+ Throttle,
+ Debounce
+}
+
+export enum HorizontalDirection {
+ Left,
+ Right
+}
+
+export function useWindowResizeHandler(
+ handler: (e: UIEvent) => void,
+ options?: Partial<{
+ wait: number
+ throttleOrDebounce: ThrottleOrDebounce
+ }>
+) {
+ if (process.server) return
+
+ const { wait = 100, throttleOrDebounce = ThrottleOrDebounce.Throttle } = options || {}
+ const finalHandler = wait
+ ? throttleOrDebounce === ThrottleOrDebounce.Throttle
+ ? throttle(handler, wait)
+ : debounce(handler, wait)
+ : handler
+
+ onMounted(() => window.addEventListener('resize', finalHandler))
+ onBeforeUnmount(() => window.removeEventListener('resize', finalHandler))
+}
+
+export function useOnBeforeWindowUnload(handler: (e: BeforeUnloadEvent) => void) {
+ onMounted(() => {
+ window.addEventListener('beforeunload', handler)
+ })
+
+ onBeforeUnmount(() => {
+ window.removeEventListener('beforeunload', handler)
+ })
+}
+
+export function useResponsiveHorizontalDirectionCalculation(params: {
+ el: MaybeRef>
+ defaultDirection?: HorizontalDirection
+ /**
+ * Stop recalculation below this screen size. Defaults to el.width * 2
+ */
+ stopUpdatesBelowWidth?: MaybeRef
+}) {
+ const { el, defaultDirection } = params
+
+ const direction = ref(
+ !isUndefined(defaultDirection) ? defaultDirection : HorizontalDirection.Right
+ )
+ const stopUpdatesBelowWidth = computed(() => {
+ const stopUpdatesBelowWidth = unref(params.stopUpdatesBelowWidth)
+ if (!isUndefined(stopUpdatesBelowWidth)) return stopUpdatesBelowWidth
+
+ const element = unref(el)
+ return element?.offsetWidth ? element.offsetWidth * 2 : undefined
+ })
+
+ const recalculateDirection = () => {
+ if (process.server) return
+ const element = unref(el)
+ if (!element) return
+
+ const rect = element.getBoundingClientRect()
+ const showOnLeftSide = rect.x + rect.width > window.innerWidth
+ const showOnRightSide = rect.x < 0
+
+ // Screen too small - do nothing
+ if (
+ (showOnLeftSide && showOnRightSide) ||
+ (!isUndefined(stopUpdatesBelowWidth.value) &&
+ window.innerWidth < stopUpdatesBelowWidth.value)
+ )
+ return
+
+ if (showOnLeftSide) {
+ direction.value = HorizontalDirection.Left
+ } else if (showOnRightSide) {
+ direction.value = HorizontalDirection.Right
+ }
+ }
+
+ useWindowResizeHandler(() => recalculateDirection())
+
+ watch(
+ () => unref(el),
+ (element) => {
+ if (element) {
+ recalculateDirection()
+ }
+ }
+ )
+
+ return {
+ direction: computed(() => direction.value),
+ recalculateDirection
+ }
+}
diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts
new file mode 100644
index 000000000..04cf2a685
--- /dev/null
+++ b/packages/frontend-2/lib/common/generated/gql/gql.ts
@@ -0,0 +1,657 @@
+/* eslint-disable */
+import * as types from './graphql';
+import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+
+/**
+ * Map of all GraphQL operations in the project.
+ *
+ * This map has several performance disadvantages:
+ * 1. It is not tree-shakeable, so it will include all operations in the project.
+ * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
+ * 3. It does not support dead code elimination, so it will add unused operations.
+ *
+ * Therefore it is highly recommended to use the babel-plugin for production.
+ */
+const documents = {
+ "\n fragment AuthRegisterPanelServerInfo on ServerInfo {\n inviteOnly\n }\n": types.AuthRegisterPanelServerInfoFragmentDoc,
+ "\n fragment ServerTermsOfServicePrivacyPolicyFragment on ServerInfo {\n termsOfService\n }\n": types.ServerTermsOfServicePrivacyPolicyFragmentFragmentDoc,
+ "\n query EmailVerificationBannerState {\n activeUser {\n id\n email\n verified\n hasPendingVerification\n }\n }\n": types.EmailVerificationBannerStateDocument,
+ "\n mutation RequestVerification {\n requestVerification\n }\n": types.RequestVerificationDocument,
+ "\n fragment AuthStategiesServerInfoFragment on ServerInfo {\n authStrategies {\n id\n name\n url\n }\n }\n": types.AuthStategiesServerInfoFragmentFragmentDoc,
+ "\n fragment CommonModelSelectorModel on Model {\n id\n name\n }\n": types.CommonModelSelectorModelFragmentDoc,
+ "\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
+ "\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc,
+ "\n fragment ProjectDiscussionsPageHeader_Project on Project {\n id\n name\n }\n": types.ProjectDiscussionsPageHeader_ProjectFragmentDoc,
+ "\n fragment ProjectDiscussionsPageResults_Project on Project {\n id\n }\n": types.ProjectDiscussionsPageResults_ProjectFragmentDoc,
+ "\n fragment ProjectModelPageHeaderProject on Project {\n id\n name\n model(id: $modelId) {\n id\n name\n }\n }\n": types.ProjectModelPageHeaderProjectFragmentDoc,
+ "\n fragment ProjectModelPageVersionsPagination on Project {\n id\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n }\n }\n": types.ProjectModelPageVersionsPaginationFragmentDoc,
+ "\n fragment ProjectModelPageVersionsProject on Project {\n id\n role\n model(id: $modelId) {\n id\n pendingImportedVersions {\n ...PendingFileUpload\n }\n }\n ...ProjectModelPageVersionsPagination\n }\n": types.ProjectModelPageVersionsProjectFragmentDoc,
+ "\n fragment ProjectModelPageDialogDeleteVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogDeleteVersionFragmentDoc,
+ "\n fragment ProjectModelPageDialogEditMessageVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogEditMessageVersionFragmentDoc,
+ "\n fragment ProjectModelPageDialogMoveToVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogMoveToVersionFragmentDoc,
+ "\n fragment ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\n }\n": types.ProjectModelPageVersionsCardVersionFragmentDoc,
+ "\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n team {\n user {\n ...FormUsersSelectItem\n }\n }\n }\n": types.ProjectModelsPageHeader_ProjectFragmentDoc,
+ "\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": types.ProjectModelsPageResults_ProjectFragmentDoc,
+ "\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n }\n": types.ProjectPageProjectHeaderFragmentDoc,
+ "\n fragment ProjectPageLatestItemsComments on Project {\n id\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageLatestItemsCommentsFragmentDoc,
+ "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\n }\n": types.ProjectPageLatestItemsCommentItemFragmentDoc,
+ "\n fragment ProjectPageLatestItemsModels on Project {\n id\n role\n modelCount: models(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageLatestItemsModelsFragmentDoc,
+ "\n fragment ProjectPageModelsActions on Model {\n id\n name\n }\n": types.ProjectPageModelsActionsFragmentDoc,
+ "\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n }\n": types.ProjectPageModelsCardProjectFragmentDoc,
+ "\n fragment ModelPreview on Model {\n previewUrl\n }\n": types.ModelPreviewFragmentDoc,
+ "\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
+ "\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": types.ProjectPageModelsCardDeleteDialogFragmentDoc,
+ "\n fragment ProjectPageModelsCardRenameDialog on Model {\n id\n name\n }\n": types.ProjectPageModelsCardRenameDialogFragmentDoc,
+ "\n fragment ProjectPageStatsBlockComments on Project {\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageStatsBlockCommentsFragmentDoc,
+ "\n fragment ProjectPageStatsBlockModels on Project {\n modelCount: models(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageStatsBlockModelsFragmentDoc,
+ "\n fragment ProjectPageStatsBlockTeam on Project {\n id\n role\n team {\n role\n user {\n ...LimitedUserAvatar\n }\n }\n ...ProjectPageTeamDialog\n }\n": types.ProjectPageStatsBlockTeamFragmentDoc,
+ "\n fragment ProjectPageStatsBlockVersions on Project {\n versionCount: versions(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageStatsBlockVersionsFragmentDoc,
+ "\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n role\n user {\n ...LimitedUserAvatar\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n }\n }\n }\n": types.ProjectPageTeamDialogFragmentDoc,
+ "\n subscription OnUserProjectsUpdate {\n userProjectsUpdated {\n type\n id\n project {\n ...ProjectDashboardItem\n }\n }\n }\n": types.OnUserProjectsUpdateDocument,
+ "\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n": types.CreateOnboardingProjectDocument,
+ "\n fragment ProjectsDashboardFilled on ProjectCollection {\n items {\n ...ProjectDashboardItem\n }\n }\n": types.ProjectsDashboardFilledFragmentDoc,
+ "\n fragment ProjectsInviteBanner on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n }\n": types.ProjectsInviteBannerFragmentDoc,
+ "\n fragment ProjectsInviteBanners on User {\n projectInvites {\n ...ProjectsInviteBanner\n }\n }\n": types.ProjectsInviteBannersFragmentDoc,
+ "\n fragment AppAuthorAvatar on AppAuthor {\n id\n name\n avatar\n }\n": types.AppAuthorAvatarFragmentDoc,
+ "\n fragment LimitedUserAvatar on LimitedUser {\n id\n name\n avatar\n }\n": types.LimitedUserAvatarFragmentDoc,
+ "\n fragment ActiveUserAvatar on User {\n id\n name\n avatar\n }\n": types.ActiveUserAvatarFragmentDoc,
+ "\n fragment UserAvatarEditable_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n": types.UserAvatarEditable_UserFragmentDoc,
+ "\n fragment UserAvatarEditor_User on User {\n id\n avatar\n }\n": types.UserAvatarEditor_UserFragmentDoc,
+ "\n fragment UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserAvatarEditable_User\n }\n": types.UserProfileEditDialogBio_UserFragmentDoc,
+ "\n fragment UserProfileEditDialogDeleteAccount_User on User {\n id\n email\n }\n": types.UserProfileEditDialogDeleteAccount_UserFragmentDoc,
+ "\n fragment UserProfileEditDialogNotificationPreferences_User on User {\n id\n notificationPreferences\n }\n": types.UserProfileEditDialogNotificationPreferences_UserFragmentDoc,
+ "\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": types.ThreadCommentAttachmentFragmentDoc,
+ "\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
+ "\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": types.ViewerModelVersionCardItemFragmentDoc,
+ "\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n }\n }\n": types.ActiveUserMainMetadataDocument,
+ "\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": types.CreateOnboardingProjectDocument,
+ "\n mutation FinishOnboarding {\n activeUserMutations {\n finishOnboarding\n }\n }\n": types.FinishOnboardingDocument,
+ "\n query AuthServerInfo {\n serverInfo {\n ...AuthStategiesServerInfoFragment\n ...ServerTermsOfServicePrivacyPolicyFragment\n ...AuthRegisterPanelServerInfo\n }\n }\n": types.AuthServerInfoDocument,
+ "\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n": types.AuthorizableAppMetadataDocument,
+ "\n query MentionsUserSearch($query: String!, $emailOnly: Boolean = false) {\n userSearch(\n query: $query\n limit: 5\n cursor: null\n archived: false\n emailOnly: $emailOnly\n ) {\n items {\n id\n name\n company\n }\n }\n }\n": types.MentionsUserSearchDocument,
+ "\n query UserSearch($query: String!, $limit: Int, $cursor: String, $archived: Boolean) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n }\n }\n }\n": types.UserSearchDocument,
+ "\n query ServerInfoBlobSizeLimit {\n serverInfo {\n blobSizeLimitBytes\n }\n }\n": types.ServerInfoBlobSizeLimitDocument,
+ "\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
+ "\n query ServerVersionInfo {\n serverInfo {\n version\n }\n }\n": types.ServerVersionInfoDocument,
+ "\n query SearchProjects($search: String, $onlyWithRoles: [String!] = null) {\n activeUser {\n projects(limit: 10, filter: { search: $search, onlyWithRoles: $onlyWithRoles }) {\n totalCount\n items {\n ...FormSelectProjects_Project\n }\n }\n }\n }\n": types.SearchProjectsDocument,
+ "\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": types.ProjectDashboardItemNoModelsFragmentDoc,
+ "\n fragment ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n models(limit: 4, filter: { onlyWithVersions: true }) {\n totalCount\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n pendingImportedModels(limit: 4) {\n ...PendingFileUpload\n }\n }\n": types.ProjectDashboardItemFragmentDoc,
+ "\n fragment PendingFileUpload on FileUpload {\n id\n projectId\n modelName\n convertedStatus\n convertedMessage\n uploadDate\n convertedLastUpdate\n fileType\n fileName\n }\n": types.PendingFileUploadFragmentDoc,
+ "\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n }\n": types.ProjectPageLatestItemsModelItemFragmentDoc,
+ "\n fragment ProjectUpdatableMetadata on Project {\n id\n name\n description\n visibility\n allowPublicComments\n }\n": types.ProjectUpdatableMetadataFragmentDoc,
+ "\n mutation CreateModel($input: CreateModelInput!) {\n modelMutations {\n create(input: $input) {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n": types.CreateModelDocument,
+ "\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n": types.CreateProjectDocument,
+ "\n mutation UpdateModel($input: UpdateModelInput!) {\n modelMutations {\n update(input: $input) {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n": types.UpdateModelDocument,
+ "\n mutation DeleteModel($input: DeleteModelInput!) {\n modelMutations {\n delete(input: $input)\n }\n }\n": types.DeleteModelDocument,
+ "\n mutation UpdateProjectRole($input: ProjectUpdateRoleInput!) {\n projectMutations {\n updateRole(input: $input) {\n id\n team {\n role\n user {\n ...LimitedUserAvatar\n }\n }\n }\n }\n }\n": types.UpdateProjectRoleDocument,
+ "\n mutation InviteProjectUser($projectId: ID!, $input: [ProjectInviteCreateInput!]!) {\n projectMutations {\n invites {\n batchCreate(projectId: $projectId, input: $input) {\n ...ProjectPageTeamDialog\n }\n }\n }\n }\n": types.InviteProjectUserDocument,
+ "\n mutation CancelProjectInvite($projectId: ID!, $inviteId: String!) {\n projectMutations {\n invites {\n cancel(projectId: $projectId, inviteId: $inviteId) {\n ...ProjectPageTeamDialog\n }\n }\n }\n }\n": types.CancelProjectInviteDocument,
+ "\n mutation UpdateProjectMetadata($update: ProjectUpdateInput!) {\n projectMutations {\n update(update: $update) {\n id\n ...ProjectUpdatableMetadata\n }\n }\n }\n": types.UpdateProjectMetadataDocument,
+ "\n mutation DeleteProject($id: String!) {\n projectMutations {\n delete(id: $id)\n }\n }\n": types.DeleteProjectDocument,
+ "\n mutation UseProjectInvite($input: ProjectInviteUseInput!) {\n projectMutations {\n invites {\n use(input: $input)\n }\n }\n }\n": types.UseProjectInviteDocument,
+ "\n mutation LeaveProject($projectId: String!) {\n projectMutations {\n leave(id: $projectId)\n }\n }\n": types.LeaveProjectDocument,
+ "\n mutation DeleteVersions($input: DeleteVersionsInput!) {\n versionMutations {\n delete(input: $input)\n }\n }\n": types.DeleteVersionsDocument,
+ "\n mutation MoveVersions($input: MoveVersionsInput!) {\n versionMutations {\n moveToModel(input: $input) {\n id\n }\n }\n }\n": types.MoveVersionsDocument,
+ "\n mutation UpdateVersion($input: UpdateVersionInput!) {\n versionMutations {\n update(input: $input) {\n id\n message\n }\n }\n }\n": types.UpdateVersionDocument,
+ "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n }\n }\n": types.ProjectAccessCheckDocument,
+ "\n query ProjectsDashboardQuery($filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(filter: $filter, limit: 6, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...ProjectDashboardItem\n }\n }\n ...ProjectsInviteBanners\n }\n }\n": types.ProjectsDashboardQueryDocument,
+ "\n query ProjectPageQuery($id: String!, $token: String) {\n project(id: $id) {\n ...ProjectPageProject\n }\n projectInvite(projectId: $id, token: $token) {\n ...ProjectsInviteBanner\n }\n }\n": types.ProjectPageQueryDocument,
+ "\n query ProjectLatestModels($projectId: String!, $filter: ProjectModelsFilter) {\n project(id: $projectId) {\n id\n models(cursor: null, limit: 16, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n pendingImportedModels {\n ...PendingFileUpload\n }\n }\n }\n": types.ProjectLatestModelsDocument,
+ "\n query ProjectLatestModelsPagination(\n $projectId: String!\n $filter: ProjectModelsFilter\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n models(cursor: $cursor, limit: 16, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n": types.ProjectLatestModelsPaginationDocument,
+ "\n query ProjectModelsTreeTopLevel(\n $projectId: String!\n $filter: ProjectModelsTreeFilter\n ) {\n project(id: $projectId) {\n id\n modelsTree(cursor: null, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...SingleLevelModelTreeItem\n }\n }\n pendingImportedModels {\n ...PendingFileUpload\n }\n }\n }\n": types.ProjectModelsTreeTopLevelDocument,
+ "\n query ProjectModelsTreeTopLevelPagination(\n $projectId: String!\n $filter: ProjectModelsTreeFilter\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n modelsTree(cursor: $cursor, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...SingleLevelModelTreeItem\n }\n }\n }\n }\n": types.ProjectModelsTreeTopLevelPaginationDocument,
+ "\n query ProjectModelChildrenTree($projectId: String!, $parentName: String!) {\n project(id: $projectId) {\n id\n modelChildrenTree(fullName: $parentName) {\n ...SingleLevelModelTreeItem\n }\n }\n }\n": types.ProjectModelChildrenTreeDocument,
+ "\n query ProjectLatestCommentThreads(\n $projectId: String!\n $cursor: String = null\n $filter: ProjectCommentsFilter = null\n ) {\n project(id: $projectId) {\n id\n commentThreads(cursor: $cursor, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsCommentItem\n }\n }\n }\n }\n": types.ProjectLatestCommentThreadsDocument,
+ "\n query ProjectInvite($projectId: String!, $token: String) {\n projectInvite(projectId: $projectId, token: $token) {\n ...ProjectsInviteBanner\n }\n }\n": types.ProjectInviteDocument,
+ "\n query ProjectModelCheck($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n id\n }\n }\n }\n": types.ProjectModelCheckDocument,
+ "\n query ProjectModelPage(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n ...ProjectModelPageHeaderProject\n ...ProjectModelPageVersionsProject\n }\n }\n": types.ProjectModelPageDocument,
+ "\n query ProjectModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n ...ProjectModelPageVersionsPagination\n }\n }\n": types.ProjectModelVersionsDocument,
+ "\n query ProjectModelsPage($projectId: String!) {\n project(id: $projectId) {\n ...ProjectModelsPageHeader_Project\n ...ProjectModelsPageResults_Project\n }\n }\n": types.ProjectModelsPageDocument,
+ "\n query ProjectDiscussionsPage($projectId: String!) {\n project(id: $projectId) {\n ...ProjectDiscussionsPageHeader_Project\n ...ProjectDiscussionsPageResults_Project\n }\n }\n": types.ProjectDiscussionsPageDocument,
+ "\n subscription OnProjectUpdated($id: String!) {\n projectUpdated(id: $id) {\n id\n type\n project {\n ...ProjectPageProject\n ...ProjectDashboardItemNoModels\n }\n }\n }\n": types.OnProjectUpdatedDocument,
+ "\n subscription OnProjectModelsUpdate($id: String!) {\n projectModelsUpdated(id: $id) {\n id\n type\n model {\n id\n versions(limit: 1) {\n items {\n id\n referencedObject\n }\n }\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n": types.OnProjectModelsUpdateDocument,
+ "\n subscription OnProjectVersionsUpdate($id: String!) {\n projectVersionsUpdated(id: $id) {\n id\n modelId\n type\n version {\n id\n ...ViewerModelVersionCardItem\n ...ProjectModelPageVersionsCardVersion\n model {\n id\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n": types.OnProjectVersionsUpdateDocument,
+ "\n subscription OnProjectVersionsPreviewGenerated($id: String!) {\n projectVersionsPreviewGenerated(id: $id) {\n projectId\n objectId\n versionId\n }\n }\n": types.OnProjectVersionsPreviewGeneratedDocument,
+ "\n subscription OnProjectPendingModelsUpdated($id: String!) {\n projectPendingModelsUpdated(id: $id) {\n id\n type\n model {\n ...PendingFileUpload\n model {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n": types.OnProjectPendingModelsUpdatedDocument,
+ "\n subscription OnProjectPendingVersionsUpdated($id: String!) {\n projectPendingVersionsUpdated(id: $id) {\n id\n type\n version {\n ...PendingFileUpload\n model {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n": types.OnProjectPendingVersionsUpdatedDocument,
+ "\n mutation InviteServerUser($input: [ServerInviteCreateInput!]!) {\n serverInviteBatchCreate(input: $input)\n }\n": types.InviteServerUserDocument,
+ "\n mutation UpdateUser($input: UserUpdateInput!) {\n activeUserMutations {\n update(user: $input) {\n id\n name\n bio\n company\n avatar\n }\n }\n }\n": types.UpdateUserDocument,
+ "\n mutation UpdateNotificationPreferences($input: JSONObject!) {\n userNotificationPreferencesUpdate(preferences: $input)\n }\n": types.UpdateNotificationPreferencesDocument,
+ "\n mutation DeleteAccount($input: UserDeleteInput!) {\n userDelete(userConfirmation: $input)\n }\n": types.DeleteAccountDocument,
+ "\n query ProfileEditDialog {\n activeUser {\n ...UserProfileEditDialogBio_User\n ...UserProfileEditDialogNotificationPreferences_User\n ...UserProfileEditDialogDeleteAccount_User\n }\n }\n": types.ProfileEditDialogDocument,
+ "\n fragment ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\n }\n": types.ViewerCommentBubblesDataFragmentDoc,
+ "\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n": types.ViewerCommentThreadFragmentDoc,
+ "\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": types.ViewerCommentsReplyItemFragmentDoc,
+ "\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": types.BroadcastViewerUserActivityDocument,
+ "\n mutation MarkCommentViewed($threadId: String!) {\n commentMutations {\n markViewed(commentId: $threadId)\n }\n }\n": types.MarkCommentViewedDocument,
+ "\n mutation CreateCommentThread($input: CreateCommentInput!) {\n commentMutations {\n create(input: $input) {\n ...ViewerCommentThread\n }\n }\n }\n": types.CreateCommentThreadDocument,
+ "\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": types.CreateCommentReplyDocument,
+ "\n mutation ArchiveComment($commentId: String!, $archived: Boolean) {\n commentMutations {\n archive(commentId: $commentId, archived: $archived)\n }\n }\n": types.ArchiveCommentDocument,
+ "\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": types.ProjectViewerResourcesDocument,
+ "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n": types.ViewerLoadedResourcesDocument,
+ "\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": types.ViewerModelVersionsDocument,
+ "\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int = 25\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": types.ViewerLoadedThreadsDocument,
+ "\n subscription OnViewerUserActivityBroadcasted($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\n }\n }\n": types.OnViewerUserActivityBroadcastedDocument,
+ "\n subscription OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\n }\n }\n }\n": types.OnViewerCommentsUpdatedDocument,
+ "\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n": types.LinkableCommentFragmentDoc,
+ "\n query GetActiveUser {\n activeUser {\n id\n name\n role\n }\n }\n": types.GetActiveUserDocument,
+ "\n fragment ProjectPageProject on Project {\n id\n createdAt\n ...ProjectPageProjectHeader\n ...ProjectPageStatsBlockTeam\n ...ProjectPageTeamDialog\n ...ProjectPageStatsBlockVersions\n ...ProjectPageStatsBlockModels\n ...ProjectPageStatsBlockComments\n ...ProjectPageLatestItemsModels\n ...ProjectPageLatestItemsComments\n }\n": types.ProjectPageProjectFragmentDoc,
+ "\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n }\n": types.ModelPageProjectFragmentDoc,
+};
+
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ *
+ *
+ * @example
+ * ```ts
+ * const query = gql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
+ * ```
+ *
+ * The query argument is unknown!
+ * Please regenerate the types.
+**/
+export function graphql(source: string): unknown;
+
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "\n fragment AuthRegisterPanelServerInfo on ServerInfo {\n inviteOnly\n }\n"): (typeof documents)["\n fragment AuthRegisterPanelServerInfo on ServerInfo {\n inviteOnly\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 ServerTermsOfServicePrivacyPolicyFragment on ServerInfo {\n termsOfService\n }\n"): (typeof documents)["\n fragment ServerTermsOfServicePrivacyPolicyFragment on ServerInfo {\n termsOfService\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 EmailVerificationBannerState {\n activeUser {\n id\n email\n verified\n hasPendingVerification\n }\n }\n"): (typeof documents)["\n query EmailVerificationBannerState {\n activeUser {\n id\n email\n verified\n hasPendingVerification\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 RequestVerification {\n requestVerification\n }\n"): (typeof documents)["\n mutation RequestVerification {\n requestVerification\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 AuthStategiesServerInfoFragment on ServerInfo {\n authStrategies {\n id\n name\n url\n }\n }\n"): (typeof documents)["\n fragment AuthStategiesServerInfoFragment on ServerInfo {\n authStrategies {\n id\n name\n url\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 CommonModelSelectorModel on Model {\n id\n name\n }\n"): (typeof documents)["\n fragment CommonModelSelectorModel on Model {\n id\n name\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 FormSelectProjects_Project on Project {\n id\n name\n }\n"): (typeof documents)["\n fragment FormSelectProjects_Project on Project {\n id\n name\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 FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n"): (typeof documents)["\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\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 ProjectDiscussionsPageHeader_Project on Project {\n id\n name\n }\n"): (typeof documents)["\n fragment ProjectDiscussionsPageHeader_Project on Project {\n id\n name\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 ProjectDiscussionsPageResults_Project on Project {\n id\n }\n"): (typeof documents)["\n fragment ProjectDiscussionsPageResults_Project on Project {\n id\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 ProjectModelPageHeaderProject on Project {\n id\n name\n model(id: $modelId) {\n id\n name\n }\n }\n"): (typeof documents)["\n fragment ProjectModelPageHeaderProject on Project {\n id\n name\n model(id: $modelId) {\n id\n name\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 ProjectModelPageVersionsPagination on Project {\n id\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectModelPageVersionsPagination on Project {\n id\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\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 ProjectModelPageVersionsProject on Project {\n id\n role\n model(id: $modelId) {\n id\n pendingImportedVersions {\n ...PendingFileUpload\n }\n }\n ...ProjectModelPageVersionsPagination\n }\n"): (typeof documents)["\n fragment ProjectModelPageVersionsProject on Project {\n id\n role\n model(id: $modelId) {\n id\n pendingImportedVersions {\n ...PendingFileUpload\n }\n }\n ...ProjectModelPageVersionsPagination\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 ProjectModelPageDialogDeleteVersion on Version {\n id\n message\n }\n"): (typeof documents)["\n fragment ProjectModelPageDialogDeleteVersion on Version {\n id\n message\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 ProjectModelPageDialogEditMessageVersion on Version {\n id\n message\n }\n"): (typeof documents)["\n fragment ProjectModelPageDialogEditMessageVersion on Version {\n id\n message\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 ProjectModelPageDialogMoveToVersion on Version {\n id\n message\n }\n"): (typeof documents)["\n fragment ProjectModelPageDialogMoveToVersion on Version {\n id\n message\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 ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\n }\n"): (typeof documents)["\n fragment ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\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 ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n team {\n user {\n ...FormUsersSelectItem\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n team {\n user {\n ...FormUsersSelectItem\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 ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n"): (typeof documents)["\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\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 ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n }\n"): (typeof documents)["\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\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 ProjectPageLatestItemsComments on Project {\n id\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsComments on Project {\n id\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\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 ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\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 ProjectPageLatestItemsModels on Project {\n id\n role\n modelCount: models(limit: 0) {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsModels on Project {\n id\n role\n modelCount: models(limit: 0) {\n totalCount\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 ProjectPageModelsActions on Model {\n id\n name\n }\n"): (typeof documents)["\n fragment ProjectPageModelsActions on Model {\n id\n name\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 ProjectPageModelsCardProject on Project {\n id\n role\n }\n"): (typeof documents)["\n fragment ProjectPageModelsCardProject on Project {\n id\n role\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 ModelPreview on Model {\n previewUrl\n }\n"): (typeof documents)["\n fragment ModelPreview on Model {\n previewUrl\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 SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\n }\n"): (typeof documents)["\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\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 ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n"): (typeof documents)["\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\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 ProjectPageModelsCardRenameDialog on Model {\n id\n name\n }\n"): (typeof documents)["\n fragment ProjectPageModelsCardRenameDialog on Model {\n id\n name\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 ProjectPageStatsBlockComments on Project {\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment ProjectPageStatsBlockComments on Project {\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\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 ProjectPageStatsBlockModels on Project {\n modelCount: models(limit: 0) {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment ProjectPageStatsBlockModels on Project {\n modelCount: models(limit: 0) {\n totalCount\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 ProjectPageStatsBlockTeam on Project {\n id\n role\n team {\n role\n user {\n ...LimitedUserAvatar\n }\n }\n ...ProjectPageTeamDialog\n }\n"): (typeof documents)["\n fragment ProjectPageStatsBlockTeam on Project {\n id\n role\n team {\n role\n user {\n ...LimitedUserAvatar\n }\n }\n ...ProjectPageTeamDialog\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 ProjectPageStatsBlockVersions on Project {\n versionCount: versions(limit: 0) {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment ProjectPageStatsBlockVersions on Project {\n versionCount: versions(limit: 0) {\n totalCount\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 ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n role\n user {\n ...LimitedUserAvatar\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n role\n user {\n ...LimitedUserAvatar\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\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 OnUserProjectsUpdate {\n userProjectsUpdated {\n type\n id\n project {\n ...ProjectDashboardItem\n }\n }\n }\n"): (typeof documents)["\n subscription OnUserProjectsUpdate {\n userProjectsUpdated {\n type\n id\n project {\n ...ProjectDashboardItem\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 CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n"): (typeof documents)["\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\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 ProjectsDashboardFilled on ProjectCollection {\n items {\n ...ProjectDashboardItem\n }\n }\n"): (typeof documents)["\n fragment ProjectsDashboardFilled on ProjectCollection {\n items {\n ...ProjectDashboardItem\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 ProjectsInviteBanner on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n }\n"): (typeof documents)["\n fragment ProjectsInviteBanner on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\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 ProjectsInviteBanners on User {\n projectInvites {\n ...ProjectsInviteBanner\n }\n }\n"): (typeof documents)["\n fragment ProjectsInviteBanners on User {\n projectInvites {\n ...ProjectsInviteBanner\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 AppAuthorAvatar on AppAuthor {\n id\n name\n avatar\n }\n"): (typeof documents)["\n fragment AppAuthorAvatar on AppAuthor {\n id\n name\n avatar\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 LimitedUserAvatar on LimitedUser {\n id\n name\n avatar\n }\n"): (typeof documents)["\n fragment LimitedUserAvatar on LimitedUser {\n id\n name\n avatar\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 ActiveUserAvatar on User {\n id\n name\n avatar\n }\n"): (typeof documents)["\n fragment ActiveUserAvatar on User {\n id\n name\n avatar\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 UserAvatarEditable_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n"): (typeof documents)["\n fragment UserAvatarEditable_User on User {\n id\n avatar\n ...ActiveUserAvatar\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 UserAvatarEditor_User on User {\n id\n avatar\n }\n"): (typeof documents)["\n fragment UserAvatarEditor_User on User {\n id\n avatar\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 UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserAvatarEditable_User\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogBio_User on User {\n id\n name\n company\n bio\n ...UserAvatarEditable_User\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 UserProfileEditDialogDeleteAccount_User on User {\n id\n email\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogDeleteAccount_User on User {\n id\n email\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 UserProfileEditDialogNotificationPreferences_User on User {\n id\n notificationPreferences\n }\n"): (typeof documents)["\n fragment UserProfileEditDialogNotificationPreferences_User on User {\n id\n notificationPreferences\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 ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n"): (typeof documents)["\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\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 ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n"): (typeof documents)["\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\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 ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n"): (typeof documents)["\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\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 ActiveUserMainMetadata {\n activeUser {\n id\n email\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n }\n }\n"): (typeof documents)["\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\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 CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n "): (typeof documents)["\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\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 FinishOnboarding {\n activeUserMutations {\n finishOnboarding\n }\n }\n"): (typeof documents)["\n mutation FinishOnboarding {\n activeUserMutations {\n finishOnboarding\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 AuthServerInfo {\n serverInfo {\n ...AuthStategiesServerInfoFragment\n ...ServerTermsOfServicePrivacyPolicyFragment\n ...AuthRegisterPanelServerInfo\n }\n }\n"): (typeof documents)["\n query AuthServerInfo {\n serverInfo {\n ...AuthStategiesServerInfoFragment\n ...ServerTermsOfServicePrivacyPolicyFragment\n ...AuthRegisterPanelServerInfo\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 AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n"): (typeof documents)["\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\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 MentionsUserSearch($query: String!, $emailOnly: Boolean = false) {\n userSearch(\n query: $query\n limit: 5\n cursor: null\n archived: false\n emailOnly: $emailOnly\n ) {\n items {\n id\n name\n company\n }\n }\n }\n"): (typeof documents)["\n query MentionsUserSearch($query: String!, $emailOnly: Boolean = false) {\n userSearch(\n query: $query\n limit: 5\n cursor: null\n archived: false\n emailOnly: $emailOnly\n ) {\n items {\n id\n name\n company\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 UserSearch($query: String!, $limit: Int, $cursor: String, $archived: Boolean) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\n }\n }\n }\n"): (typeof documents)["\n query UserSearch($query: String!, $limit: Int, $cursor: String, $archived: Boolean) {\n userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived) {\n cursor\n items {\n id\n name\n bio\n company\n avatar\n verified\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 ServerInfoBlobSizeLimit {\n serverInfo {\n blobSizeLimitBytes\n }\n }\n"): (typeof documents)["\n query ServerInfoBlobSizeLimit {\n serverInfo {\n blobSizeLimitBytes\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 ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\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 ServerVersionInfo {\n serverInfo {\n version\n }\n }\n"): (typeof documents)["\n query ServerVersionInfo {\n serverInfo {\n version\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 SearchProjects($search: String, $onlyWithRoles: [String!] = null) {\n activeUser {\n projects(limit: 10, filter: { search: $search, onlyWithRoles: $onlyWithRoles }) {\n totalCount\n items {\n ...FormSelectProjects_Project\n }\n }\n }\n }\n"): (typeof documents)["\n query SearchProjects($search: String, $onlyWithRoles: [String!] = null) {\n activeUser {\n projects(limit: 10, filter: { search: $search, onlyWithRoles: $onlyWithRoles }) {\n totalCount\n items {\n ...FormSelectProjects_Project\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 ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n"): (typeof documents)["\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\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 ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n models(limit: 4, filter: { onlyWithVersions: true }) {\n totalCount\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n pendingImportedModels(limit: 4) {\n ...PendingFileUpload\n }\n }\n"): (typeof documents)["\n fragment ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n models(limit: 4, filter: { onlyWithVersions: true }) {\n totalCount\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n pendingImportedModels(limit: 4) {\n ...PendingFileUpload\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 PendingFileUpload on FileUpload {\n id\n projectId\n modelName\n convertedStatus\n convertedMessage\n uploadDate\n convertedLastUpdate\n fileType\n fileName\n }\n"): (typeof documents)["\n fragment PendingFileUpload on FileUpload {\n id\n projectId\n modelName\n convertedStatus\n convertedMessage\n uploadDate\n convertedLastUpdate\n fileType\n fileName\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 ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\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 ProjectUpdatableMetadata on Project {\n id\n name\n description\n visibility\n allowPublicComments\n }\n"): (typeof documents)["\n fragment ProjectUpdatableMetadata on Project {\n id\n name\n description\n visibility\n allowPublicComments\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 CreateModel($input: CreateModelInput!) {\n modelMutations {\n create(input: $input) {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n"): (typeof documents)["\n mutation CreateModel($input: CreateModelInput!) {\n modelMutations {\n create(input: $input) {\n ...ProjectPageLatestItemsModelItem\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 CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n"): (typeof documents)["\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectPageProject\n ...ProjectDashboardItem\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 UpdateModel($input: UpdateModelInput!) {\n modelMutations {\n update(input: $input) {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateModel($input: UpdateModelInput!) {\n modelMutations {\n update(input: $input) {\n ...ProjectPageLatestItemsModelItem\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 DeleteModel($input: DeleteModelInput!) {\n modelMutations {\n delete(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DeleteModel($input: DeleteModelInput!) {\n modelMutations {\n delete(input: $input)\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 UpdateProjectRole($input: ProjectUpdateRoleInput!) {\n projectMutations {\n updateRole(input: $input) {\n id\n team {\n role\n user {\n ...LimitedUserAvatar\n }\n }\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateProjectRole($input: ProjectUpdateRoleInput!) {\n projectMutations {\n updateRole(input: $input) {\n id\n team {\n role\n user {\n ...LimitedUserAvatar\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 InviteProjectUser($projectId: ID!, $input: [ProjectInviteCreateInput!]!) {\n projectMutations {\n invites {\n batchCreate(projectId: $projectId, input: $input) {\n ...ProjectPageTeamDialog\n }\n }\n }\n }\n"): (typeof documents)["\n mutation InviteProjectUser($projectId: ID!, $input: [ProjectInviteCreateInput!]!) {\n projectMutations {\n invites {\n batchCreate(projectId: $projectId, input: $input) {\n ...ProjectPageTeamDialog\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 CancelProjectInvite($projectId: ID!, $inviteId: String!) {\n projectMutations {\n invites {\n cancel(projectId: $projectId, inviteId: $inviteId) {\n ...ProjectPageTeamDialog\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CancelProjectInvite($projectId: ID!, $inviteId: String!) {\n projectMutations {\n invites {\n cancel(projectId: $projectId, inviteId: $inviteId) {\n ...ProjectPageTeamDialog\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 UpdateProjectMetadata($update: ProjectUpdateInput!) {\n projectMutations {\n update(update: $update) {\n id\n ...ProjectUpdatableMetadata\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateProjectMetadata($update: ProjectUpdateInput!) {\n projectMutations {\n update(update: $update) {\n id\n ...ProjectUpdatableMetadata\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 DeleteProject($id: String!) {\n projectMutations {\n delete(id: $id)\n }\n }\n"): (typeof documents)["\n mutation DeleteProject($id: String!) {\n projectMutations {\n delete(id: $id)\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 UseProjectInvite($input: ProjectInviteUseInput!) {\n projectMutations {\n invites {\n use(input: $input)\n }\n }\n }\n"): (typeof documents)["\n mutation UseProjectInvite($input: ProjectInviteUseInput!) {\n projectMutations {\n invites {\n use(input: $input)\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 LeaveProject($projectId: String!) {\n projectMutations {\n leave(id: $projectId)\n }\n }\n"): (typeof documents)["\n mutation LeaveProject($projectId: String!) {\n projectMutations {\n leave(id: $projectId)\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 DeleteVersions($input: DeleteVersionsInput!) {\n versionMutations {\n delete(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DeleteVersions($input: DeleteVersionsInput!) {\n versionMutations {\n delete(input: $input)\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 MoveVersions($input: MoveVersionsInput!) {\n versionMutations {\n moveToModel(input: $input) {\n id\n }\n }\n }\n"): (typeof documents)["\n mutation MoveVersions($input: MoveVersionsInput!) {\n versionMutations {\n moveToModel(input: $input) {\n id\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 UpdateVersion($input: UpdateVersionInput!) {\n versionMutations {\n update(input: $input) {\n id\n message\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateVersion($input: UpdateVersionInput!) {\n versionMutations {\n update(input: $input) {\n id\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.
+ */
+export function graphql(source: "\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\n }\n }\n"): (typeof documents)["\n query ProjectAccessCheck($id: String!) {\n project(id: $id) {\n id\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 ProjectsDashboardQuery($filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(filter: $filter, limit: 6, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...ProjectDashboardItem\n }\n }\n ...ProjectsInviteBanners\n }\n }\n"): (typeof documents)["\n query ProjectsDashboardQuery($filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(filter: $filter, limit: 6, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...ProjectDashboardItem\n }\n }\n ...ProjectsInviteBanners\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 ProjectPageQuery($id: String!, $token: String) {\n project(id: $id) {\n ...ProjectPageProject\n }\n projectInvite(projectId: $id, token: $token) {\n ...ProjectsInviteBanner\n }\n }\n"): (typeof documents)["\n query ProjectPageQuery($id: String!, $token: String) {\n project(id: $id) {\n ...ProjectPageProject\n }\n projectInvite(projectId: $id, token: $token) {\n ...ProjectsInviteBanner\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 ProjectLatestModels($projectId: String!, $filter: ProjectModelsFilter) {\n project(id: $projectId) {\n id\n models(cursor: null, limit: 16, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n pendingImportedModels {\n ...PendingFileUpload\n }\n }\n }\n"): (typeof documents)["\n query ProjectLatestModels($projectId: String!, $filter: ProjectModelsFilter) {\n project(id: $projectId) {\n id\n models(cursor: null, limit: 16, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n pendingImportedModels {\n ...PendingFileUpload\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 ProjectLatestModelsPagination(\n $projectId: String!\n $filter: ProjectModelsFilter\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n models(cursor: $cursor, limit: 16, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectLatestModelsPagination(\n $projectId: String!\n $filter: ProjectModelsFilter\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n models(cursor: $cursor, limit: 16, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsModelItem\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 ProjectModelsTreeTopLevel(\n $projectId: String!\n $filter: ProjectModelsTreeFilter\n ) {\n project(id: $projectId) {\n id\n modelsTree(cursor: null, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...SingleLevelModelTreeItem\n }\n }\n pendingImportedModels {\n ...PendingFileUpload\n }\n }\n }\n"): (typeof documents)["\n query ProjectModelsTreeTopLevel(\n $projectId: String!\n $filter: ProjectModelsTreeFilter\n ) {\n project(id: $projectId) {\n id\n modelsTree(cursor: null, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...SingleLevelModelTreeItem\n }\n }\n pendingImportedModels {\n ...PendingFileUpload\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 ProjectModelsTreeTopLevelPagination(\n $projectId: String!\n $filter: ProjectModelsTreeFilter\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n modelsTree(cursor: $cursor, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...SingleLevelModelTreeItem\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectModelsTreeTopLevelPagination(\n $projectId: String!\n $filter: ProjectModelsTreeFilter\n $cursor: String = null\n ) {\n project(id: $projectId) {\n id\n modelsTree(cursor: $cursor, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...SingleLevelModelTreeItem\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 ProjectModelChildrenTree($projectId: String!, $parentName: String!) {\n project(id: $projectId) {\n id\n modelChildrenTree(fullName: $parentName) {\n ...SingleLevelModelTreeItem\n }\n }\n }\n"): (typeof documents)["\n query ProjectModelChildrenTree($projectId: String!, $parentName: String!) {\n project(id: $projectId) {\n id\n modelChildrenTree(fullName: $parentName) {\n ...SingleLevelModelTreeItem\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 ProjectLatestCommentThreads(\n $projectId: String!\n $cursor: String = null\n $filter: ProjectCommentsFilter = null\n ) {\n project(id: $projectId) {\n id\n commentThreads(cursor: $cursor, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsCommentItem\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectLatestCommentThreads(\n $projectId: String!\n $cursor: String = null\n $filter: ProjectCommentsFilter = null\n ) {\n project(id: $projectId) {\n id\n commentThreads(cursor: $cursor, limit: 8, filter: $filter) {\n totalCount\n cursor\n items {\n ...ProjectPageLatestItemsCommentItem\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 ProjectInvite($projectId: String!, $token: String) {\n projectInvite(projectId: $projectId, token: $token) {\n ...ProjectsInviteBanner\n }\n }\n"): (typeof documents)["\n query ProjectInvite($projectId: String!, $token: String) {\n projectInvite(projectId: $projectId, token: $token) {\n ...ProjectsInviteBanner\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 ProjectModelCheck($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n id\n }\n }\n }\n"): (typeof documents)["\n query ProjectModelCheck($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n id\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 ProjectModelPage(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n ...ProjectModelPageHeaderProject\n ...ProjectModelPageVersionsProject\n }\n }\n"): (typeof documents)["\n query ProjectModelPage(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n ...ProjectModelPageHeaderProject\n ...ProjectModelPageVersionsProject\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 ProjectModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n ...ProjectModelPageVersionsPagination\n }\n }\n"): (typeof documents)["\n query ProjectModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n ...ProjectModelPageVersionsPagination\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 ProjectModelsPage($projectId: String!) {\n project(id: $projectId) {\n ...ProjectModelsPageHeader_Project\n ...ProjectModelsPageResults_Project\n }\n }\n"): (typeof documents)["\n query ProjectModelsPage($projectId: String!) {\n project(id: $projectId) {\n ...ProjectModelsPageHeader_Project\n ...ProjectModelsPageResults_Project\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 ProjectDiscussionsPage($projectId: String!) {\n project(id: $projectId) {\n ...ProjectDiscussionsPageHeader_Project\n ...ProjectDiscussionsPageResults_Project\n }\n }\n"): (typeof documents)["\n query ProjectDiscussionsPage($projectId: String!) {\n project(id: $projectId) {\n ...ProjectDiscussionsPageHeader_Project\n ...ProjectDiscussionsPageResults_Project\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 OnProjectUpdated($id: String!) {\n projectUpdated(id: $id) {\n id\n type\n project {\n ...ProjectPageProject\n ...ProjectDashboardItemNoModels\n }\n }\n }\n"): (typeof documents)["\n subscription OnProjectUpdated($id: String!) {\n projectUpdated(id: $id) {\n id\n type\n project {\n ...ProjectPageProject\n ...ProjectDashboardItemNoModels\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 OnProjectModelsUpdate($id: String!) {\n projectModelsUpdated(id: $id) {\n id\n type\n model {\n id\n versions(limit: 1) {\n items {\n id\n referencedObject\n }\n }\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n"): (typeof documents)["\n subscription OnProjectModelsUpdate($id: String!) {\n projectModelsUpdated(id: $id) {\n id\n type\n model {\n id\n versions(limit: 1) {\n items {\n id\n referencedObject\n }\n }\n ...ProjectPageLatestItemsModelItem\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 OnProjectVersionsUpdate($id: String!) {\n projectVersionsUpdated(id: $id) {\n id\n modelId\n type\n version {\n id\n ...ViewerModelVersionCardItem\n ...ProjectModelPageVersionsCardVersion\n model {\n id\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n"): (typeof documents)["\n subscription OnProjectVersionsUpdate($id: String!) {\n projectVersionsUpdated(id: $id) {\n id\n modelId\n type\n version {\n id\n ...ViewerModelVersionCardItem\n ...ProjectModelPageVersionsCardVersion\n model {\n id\n ...ProjectPageLatestItemsModelItem\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 OnProjectVersionsPreviewGenerated($id: String!) {\n projectVersionsPreviewGenerated(id: $id) {\n projectId\n objectId\n versionId\n }\n }\n"): (typeof documents)["\n subscription OnProjectVersionsPreviewGenerated($id: String!) {\n projectVersionsPreviewGenerated(id: $id) {\n projectId\n objectId\n versionId\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 OnProjectPendingModelsUpdated($id: String!) {\n projectPendingModelsUpdated(id: $id) {\n id\n type\n model {\n ...PendingFileUpload\n model {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n"): (typeof documents)["\n subscription OnProjectPendingModelsUpdated($id: String!) {\n projectPendingModelsUpdated(id: $id) {\n id\n type\n model {\n ...PendingFileUpload\n model {\n ...ProjectPageLatestItemsModelItem\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 OnProjectPendingVersionsUpdated($id: String!) {\n projectPendingVersionsUpdated(id: $id) {\n id\n type\n version {\n ...PendingFileUpload\n model {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n"): (typeof documents)["\n subscription OnProjectPendingVersionsUpdated($id: String!) {\n projectPendingVersionsUpdated(id: $id) {\n id\n type\n version {\n ...PendingFileUpload\n model {\n ...ProjectPageLatestItemsModelItem\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 InviteServerUser($input: [ServerInviteCreateInput!]!) {\n serverInviteBatchCreate(input: $input)\n }\n"): (typeof documents)["\n mutation InviteServerUser($input: [ServerInviteCreateInput!]!) {\n serverInviteBatchCreate(input: $input)\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 UpdateUser($input: UserUpdateInput!) {\n activeUserMutations {\n update(user: $input) {\n id\n name\n bio\n company\n avatar\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateUser($input: UserUpdateInput!) {\n activeUserMutations {\n update(user: $input) {\n id\n name\n bio\n company\n avatar\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 UpdateNotificationPreferences($input: JSONObject!) {\n userNotificationPreferencesUpdate(preferences: $input)\n }\n"): (typeof documents)["\n mutation UpdateNotificationPreferences($input: JSONObject!) {\n userNotificationPreferencesUpdate(preferences: $input)\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 DeleteAccount($input: UserDeleteInput!) {\n userDelete(userConfirmation: $input)\n }\n"): (typeof documents)["\n mutation DeleteAccount($input: UserDeleteInput!) {\n userDelete(userConfirmation: $input)\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 ProfileEditDialog {\n activeUser {\n ...UserProfileEditDialogBio_User\n ...UserProfileEditDialogNotificationPreferences_User\n ...UserProfileEditDialogDeleteAccount_User\n }\n }\n"): (typeof documents)["\n query ProfileEditDialog {\n activeUser {\n ...UserProfileEditDialogBio_User\n ...UserProfileEditDialogNotificationPreferences_User\n ...UserProfileEditDialogDeleteAccount_User\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 ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\n }\n"): (typeof documents)["\n fragment ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\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 ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n"): (typeof documents)["\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\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 ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n"): (typeof documents)["\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\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 BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n"): (typeof documents)["\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\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 MarkCommentViewed($threadId: String!) {\n commentMutations {\n markViewed(commentId: $threadId)\n }\n }\n"): (typeof documents)["\n mutation MarkCommentViewed($threadId: String!) {\n commentMutations {\n markViewed(commentId: $threadId)\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 CreateCommentThread($input: CreateCommentInput!) {\n commentMutations {\n create(input: $input) {\n ...ViewerCommentThread\n }\n }\n }\n"): (typeof documents)["\n mutation CreateCommentThread($input: CreateCommentInput!) {\n commentMutations {\n create(input: $input) {\n ...ViewerCommentThread\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 CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n"): (typeof documents)["\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\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 ArchiveComment($commentId: String!, $archived: Boolean) {\n commentMutations {\n archive(commentId: $commentId, archived: $archived)\n }\n }\n"): (typeof documents)["\n mutation ArchiveComment($commentId: String!, $archived: Boolean) {\n commentMutations {\n archive(commentId: $commentId, archived: $archived)\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 ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\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 ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\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 ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\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 query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int = 25\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n"): (typeof documents)["\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int = 25\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\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 OnViewerUserActivityBroadcasted($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\n }\n }\n"): (typeof documents)["\n subscription OnViewerUserActivityBroadcasted($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\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 OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\n }\n }\n }\n"): (typeof documents)["\n subscription OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\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 LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n"): (typeof documents)["\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\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 GetActiveUser {\n activeUser {\n id\n name\n role\n }\n }\n"): (typeof documents)["\n query GetActiveUser {\n activeUser {\n id\n name\n role\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 ProjectPageProject on Project {\n id\n createdAt\n ...ProjectPageProjectHeader\n ...ProjectPageStatsBlockTeam\n ...ProjectPageTeamDialog\n ...ProjectPageStatsBlockVersions\n ...ProjectPageStatsBlockModels\n ...ProjectPageStatsBlockComments\n ...ProjectPageLatestItemsModels\n ...ProjectPageLatestItemsComments\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n ...ProjectPageProjectHeader\n ...ProjectPageStatsBlockTeam\n ...ProjectPageTeamDialog\n ...ProjectPageStatsBlockVersions\n ...ProjectPageStatsBlockModels\n ...ProjectPageStatsBlockComments\n ...ProjectPageLatestItemsModels\n ...ProjectPageLatestItemsComments\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 ModelPageProject on Project {\n id\n createdAt\n name\n }\n"): (typeof documents)["\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n }\n"];
+
+export function graphql(source: string) {
+ return (documents as any)[source] ?? {};
+}
+
+export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
\ No newline at end of file
diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts
new file mode 100644
index 000000000..f7dc44655
--- /dev/null
+++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts
@@ -0,0 +1,3339 @@
+/* eslint-disable */
+import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+export type Maybe = T | null;
+export type InputMaybe = Maybe;
+export type Exact = { [K in keyof T]: T[K] };
+export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
+export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+ ID: string;
+ String: string;
+ Boolean: boolean;
+ Int: number;
+ Float: number;
+ /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */
+ BigInt: any;
+ /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */
+ DateTime: string;
+ EmailAddress: any;
+ /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
+ JSONObject: {};
+};
+
+export type ActiveUserMutations = {
+ __typename?: 'ActiveUserMutations';
+ /** Mark onboarding as complete */
+ finishOnboarding: Scalars['Boolean'];
+ /** Edit a user's profile */
+ update: User;
+};
+
+
+export type ActiveUserMutationsUpdateArgs = {
+ user: UserUpdateInput;
+};
+
+export type Activity = {
+ __typename?: 'Activity';
+ actionType: Scalars['String'];
+ id: Scalars['ID'];
+ info: Scalars['JSONObject'];
+ message: Scalars['String'];
+ resourceId: Scalars['String'];
+ resourceType: Scalars['String'];
+ streamId?: Maybe;
+ time: Scalars['DateTime'];
+ userId: Scalars['String'];
+};
+
+export type ActivityCollection = {
+ __typename?: 'ActivityCollection';
+ cursor?: Maybe;
+ items?: Maybe>>;
+ totalCount: Scalars['Int'];
+};
+
+export type AdminUsersListCollection = {
+ __typename?: 'AdminUsersListCollection';
+ items: Array;
+ totalCount: Scalars['Int'];
+};
+
+/**
+ * A representation of a registered or invited user in the admin users list. Either registeredUser
+ * or invitedUser will always be set, both values can't be null.
+ */
+export type AdminUsersListItem = {
+ __typename?: 'AdminUsersListItem';
+ id: Scalars['String'];
+ invitedUser?: Maybe;
+ registeredUser?: Maybe;
+};
+
+export type ApiToken = {
+ __typename?: 'ApiToken';
+ createdAt: Scalars['DateTime'];
+ id: Scalars['String'];
+ lastChars: Scalars['String'];
+ lastUsed: Scalars['DateTime'];
+ lifespan: Scalars['BigInt'];
+ name: Scalars['String'];
+ scopes: Array>;
+};
+
+export type ApiTokenCreateInput = {
+ lifespan?: InputMaybe;
+ name: Scalars['String'];
+ scopes: Array;
+};
+
+export type AppAuthor = {
+ __typename?: 'AppAuthor';
+ avatar?: Maybe;
+ id?: Maybe;
+ name?: Maybe;
+};
+
+export type AppCreateInput = {
+ description: Scalars['String'];
+ logo?: InputMaybe;
+ name: Scalars['String'];
+ public?: InputMaybe;
+ redirectUrl: Scalars['String'];
+ scopes: Array>;
+ termsAndConditionsLink?: InputMaybe;
+};
+
+export type AppUpdateInput = {
+ description: Scalars['String'];
+ id: Scalars['String'];
+ logo?: InputMaybe;
+ name: Scalars['String'];
+ public?: InputMaybe;
+ redirectUrl: Scalars['String'];
+ scopes: Array>;
+ termsAndConditionsLink?: InputMaybe;
+};
+
+export type AuthStrategy = {
+ __typename?: 'AuthStrategy';
+ color?: Maybe;
+ icon: Scalars['String'];
+ id: Scalars['String'];
+ name: Scalars['String'];
+ url: Scalars['String'];
+};
+
+export type BlobMetadata = {
+ __typename?: 'BlobMetadata';
+ createdAt: Scalars['DateTime'];
+ fileHash?: Maybe;
+ fileName: Scalars['String'];
+ fileSize?: Maybe;
+ fileType: Scalars['String'];
+ id: Scalars['String'];
+ streamId: Scalars['String'];
+ uploadError?: Maybe;
+ uploadStatus: Scalars['Int'];
+ userId: Scalars['String'];
+};
+
+export type BlobMetadataCollection = {
+ __typename?: 'BlobMetadataCollection';
+ cursor?: Maybe;
+ items?: Maybe>;
+ totalCount: Scalars['Int'];
+ totalSize: Scalars['Int'];
+};
+
+export type Branch = {
+ __typename?: 'Branch';
+ /** All the recent activity on this branch in chronological order */
+ activity?: Maybe;
+ author?: Maybe;
+ commits?: Maybe;
+ createdAt?: Maybe;
+ description?: Maybe;
+ id: Scalars['String'];
+ name: Scalars['String'];
+};
+
+
+export type BranchActivityArgs = {
+ actionType?: InputMaybe;
+ after?: InputMaybe;
+ before?: InputMaybe;
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+export type BranchCommitsArgs = {
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+export type BranchCollection = {
+ __typename?: 'BranchCollection';
+ cursor?: Maybe;
+ items?: Maybe>;
+ totalCount: Scalars['Int'];
+};
+
+export type BranchCreateInput = {
+ description?: InputMaybe;
+ name: Scalars['String'];
+ streamId: Scalars['String'];
+};
+
+export type BranchDeleteInput = {
+ id: Scalars['String'];
+ streamId: Scalars['String'];
+};
+
+export type BranchUpdateInput = {
+ description?: InputMaybe;
+ id: Scalars['String'];
+ name?: InputMaybe;
+ streamId: Scalars['String'];
+};
+
+export type Comment = {
+ __typename?: 'Comment';
+ archived: Scalars['Boolean'];
+ author: LimitedUser;
+ authorId: Scalars['String'];
+ createdAt: Scalars['DateTime'];
+ /**
+ * Legacy comment viewer data field
+ * @deprecated Use the new viewerState field instead
+ */
+ data?: Maybe;
+ /** Whether or not comment is a reply to another comment */
+ hasParent: Scalars['Boolean'];
+ id: Scalars['String'];
+ /** Parent thread, if there's any */
+ parent?: Maybe;
+ /** Plain-text version of the comment text, ideal for previews */
+ rawText: Scalars['String'];
+ /** @deprecated Not actually implemented */
+ reactions?: Maybe>>;
+ /** Gets the replies to this comment. */
+ replies: CommentCollection;
+ /** Get authors of replies to this comment */
+ replyAuthors: CommentReplyAuthorCollection;
+ /** Resources that this comment targets. Can be a mixture of either one stream, or multiple commits and objects. */
+ resources: Array;
+ screenshot?: Maybe;
+ text: SmartTextEditorValue;
+ /** The time this comment was last updated. Corresponds also to the latest reply to this comment, if any. */
+ updatedAt: Scalars['DateTime'];
+ /** The last time you viewed this comment. Present only if an auth'ed request. Relevant only if a top level commit. */
+ viewedAt?: Maybe;
+ /** Resource identifiers as defined and implemented in the Viewer of the new frontend */
+ viewerResources: Array;
+ /** SerializedViewerState */
+ viewerState?: Maybe;
+};
+
+
+export type CommentRepliesArgs = {
+ cursor?: InputMaybe;
+ limit?: InputMaybe;
+};
+
+
+export type CommentReplyAuthorsArgs = {
+ limit?: Scalars['Int'];
+};
+
+export type CommentActivityMessage = {
+ __typename?: 'CommentActivityMessage';
+ comment: Comment;
+ type: Scalars['String'];
+};
+
+export type CommentCollection = {
+ __typename?: 'CommentCollection';
+ cursor?: Maybe;
+ items: Array;
+ totalCount: Scalars['Int'];
+};
+
+export type CommentContentInput = {
+ blobIds?: InputMaybe>;
+ doc?: InputMaybe;
+};
+
+/** Deprecated: Used by old stream-based mutations */
+export type CommentCreateInput = {
+ /** IDs of uploaded blobs that should be attached to this comment */
+ blobIds: Array;
+ data: Scalars['JSONObject'];
+ /**
+ * Specifies the resources this comment is linked to. There are several use cases:
+ * - a comment targets only one resource (commit or object)
+ * - a comment targets one or more resources (commits or objects)
+ * - a comment targets only a stream
+ */
+ resources: Array>;
+ screenshot?: InputMaybe;
+ streamId: Scalars['String'];
+ /** ProseMirror document object */
+ text?: InputMaybe;
+};
+
+export type CommentDataFilters = {
+ __typename?: 'CommentDataFilters';
+ hiddenIds?: Maybe>;
+ isolatedIds?: Maybe>;
+ passMax?: Maybe;
+ passMin?: Maybe;
+ propertyInfoKey?: Maybe;
+ sectionBox?: Maybe;
+};
+
+/** Equivalent to frontend-1's LocalFilterState */
+export type CommentDataFiltersInput = {
+ hiddenIds?: InputMaybe>;
+ isolatedIds?: InputMaybe>;
+ passMax?: InputMaybe;
+ passMin?: InputMaybe;
+ propertyInfoKey?: InputMaybe;
+ sectionBox?: InputMaybe;
+};
+
+/** Deprecated: Used by old stream-based mutations */
+export type CommentEditInput = {
+ /** IDs of uploaded blobs that should be attached to this comment */
+ blobIds: Array;
+ id: Scalars['String'];
+ streamId: Scalars['String'];
+ /** ProseMirror document object */
+ text?: InputMaybe;
+};
+
+export type CommentMutations = {
+ __typename?: 'CommentMutations';
+ archive: Scalars['Boolean'];
+ create: Comment;
+ edit: Comment;
+ markViewed: Scalars['Boolean'];
+ reply: Comment;
+};
+
+
+export type CommentMutationsArchiveArgs = {
+ archived?: Scalars['Boolean'];
+ commentId: Scalars['String'];
+};
+
+
+export type CommentMutationsCreateArgs = {
+ input: CreateCommentInput;
+};
+
+
+export type CommentMutationsEditArgs = {
+ input: EditCommentInput;
+};
+
+
+export type CommentMutationsMarkViewedArgs = {
+ commentId: Scalars['String'];
+};
+
+
+export type CommentMutationsReplyArgs = {
+ input: CreateCommentReplyInput;
+};
+
+export type CommentReplyAuthorCollection = {
+ __typename?: 'CommentReplyAuthorCollection';
+ items: Array;
+ totalCount: Scalars['Int'];
+};
+
+export type CommentThreadActivityMessage = {
+ __typename?: 'CommentThreadActivityMessage';
+ data?: Maybe;
+ reply?: Maybe;
+ type: Scalars['String'];
+};
+
+export type Commit = {
+ __typename?: 'Commit';
+ /** All the recent activity on this commit in chronological order */
+ activity?: Maybe;
+ authorAvatar?: Maybe;
+ authorId?: Maybe;
+ authorName?: Maybe;
+ branchName?: Maybe;
+ /**
+ * The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id.
+ * E.g.,
+ * ```
+ * query{
+ * comments(streamId:"streamId" resources:[{resourceType: commit, resourceId:"commitId"}] ){
+ * ...
+ * }
+ * ```
+ */
+ commentCount: Scalars['Int'];
+ createdAt?: Maybe;
+ id: Scalars['String'];
+ message?: Maybe;
+ parents?: Maybe>>;
+ referencedObject: Scalars['String'];
+ sourceApplication?: Maybe;
+ /**
+ * Will throw an authorization error if active user isn't authorized to see it, for example,
+ * if a stream isn't public and the user doesn't have the appropriate rights.
+ */
+ stream: Stream;
+ /** @deprecated Use the stream field instead */
+ streamId?: Maybe;
+ /** @deprecated Use the stream field instead */
+ streamName?: Maybe;
+ totalChildrenCount?: Maybe;
+};
+
+
+export type CommitActivityArgs = {
+ actionType?: InputMaybe;
+ after?: InputMaybe;
+ before?: InputMaybe;
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+export type CommitCollection = {
+ __typename?: 'CommitCollection';
+ cursor?: Maybe;
+ items?: Maybe>;
+ totalCount: Scalars['Int'];
+};
+
+export type CommitCreateInput = {
+ branchName: Scalars['String'];
+ message?: InputMaybe;
+ objectId: Scalars['String'];
+ parents?: InputMaybe>>;
+ sourceApplication?: InputMaybe;
+ streamId: Scalars['String'];
+ totalChildrenCount?: InputMaybe;
+};
+
+export type CommitDeleteInput = {
+ id: Scalars['String'];
+ streamId: Scalars['String'];
+};
+
+export type CommitReceivedInput = {
+ commitId: Scalars['String'];
+ message?: InputMaybe;
+ sourceApplication: Scalars['String'];
+ streamId: Scalars['String'];
+};
+
+export type CommitUpdateInput = {
+ id: Scalars['String'];
+ message?: InputMaybe;
+ /** To move the commit to a different branch, please the name of the branch. */
+ newBranchName?: InputMaybe;
+ streamId: Scalars['String'];
+};
+
+export type CommitsDeleteInput = {
+ commitIds: Array;
+};
+
+export type CommitsMoveInput = {
+ commitIds: Array;
+ targetBranch: Scalars['String'];
+};
+
+export type CreateCommentInput = {
+ content: CommentContentInput;
+ projectId: Scalars['String'];
+ /** Resources that this comment should be attached to */
+ resourceIdString: Scalars['String'];
+ screenshot?: InputMaybe;
+ /**
+ * SerializedViewerState. If omitted, comment won't render (correctly) inside the
+ * viewer, but will still be retrievable through the API
+ */
+ viewerState?: InputMaybe;
+};
+
+export type CreateCommentReplyInput = {
+ content: CommentContentInput;
+ threadId: Scalars['String'];
+};
+
+export type CreateModelInput = {
+ name: Scalars['String'];
+ projectId: Scalars['ID'];
+};
+
+export type DeleteModelInput = {
+ id: Scalars['ID'];
+ projectId: Scalars['ID'];
+};
+
+export type DeleteVersionsInput = {
+ versionIds: Array;
+};
+
+export enum DiscoverableStreamsSortType {
+ CreatedDate = 'CREATED_DATE',
+ FavoritesCount = 'FAVORITES_COUNT'
+}
+
+export type DiscoverableStreamsSortingInput = {
+ direction: SortDirection;
+ type: DiscoverableStreamsSortType;
+};
+
+export type EditCommentInput = {
+ commentId: Scalars['String'];
+ content: CommentContentInput;
+};
+
+export type FileUpload = {
+ __typename?: 'FileUpload';
+ branchName: Scalars['String'];
+ /** If present, the conversion result is stored in this commit. */
+ convertedCommitId?: Maybe;
+ convertedLastUpdate: Scalars['DateTime'];
+ /** Holds any errors or info. */
+ convertedMessage?: Maybe;
+ /** 0 = queued, 1 = processing, 2 = success, 3 = error */
+ convertedStatus: Scalars['Int'];
+ /** Alias for convertedCommitId */
+ convertedVersionId?: Maybe;
+ fileName: Scalars['String'];
+ fileSize: Scalars['Int'];
+ fileType: Scalars['String'];
+ id: Scalars['String'];
+ /** Model associated with the file upload, if it exists already */
+ model?: Maybe;
+ /** Alias for branchName */
+ modelName: Scalars['String'];
+ /** Alias for streamId */
+ projectId: Scalars['String'];
+ streamId: Scalars['String'];
+ uploadComplete: Scalars['Boolean'];
+ uploadDate: Scalars['DateTime'];
+ /** The user's id that uploaded this file. */
+ userId: Scalars['String'];
+};
+
+export type LegacyCommentViewerData = {
+ __typename?: 'LegacyCommentViewerData';
+ /**
+ * An array representing a user's camera position:
+ * [camPos.x, camPos.y, camPos.z, camTarget.x, camTarget.y, camTarget.z, isOrtho, zoomNumber]
+ */
+ camPos: Array;
+ /** Old FE LocalFilterState type */
+ filters: CommentDataFilters;
+ /** THREE.Vector3 {x, y, z} */
+ location: Scalars['JSONObject'];
+ /** Viewer.getCurrentSectionBox(): THREE.Box3 */
+ sectionBox?: Maybe;
+ /** Currently unused. Ideally comments should keep track of selected objects. */
+ selection?: Maybe;
+};
+
+/**
+ * Limited user type, for showing public info about a user
+ * to another user
+ */
+export type LimitedUser = {
+ __typename?: 'LimitedUser';
+ /** All the recent activity from this user in chronological order */
+ activity?: Maybe;
+ avatar?: Maybe;
+ bio?: Maybe;
+ /** Get public stream commits authored by the user */
+ commits?: Maybe;
+ company?: Maybe;
+ id: Scalars['ID'];
+ name: Scalars['String'];
+ role?: Maybe;
+ /** Returns all discoverable streams that the user is a collaborator on */
+ streams: StreamCollection;
+ /** The user's timeline in chronological order */
+ timeline?: Maybe;
+ /** Total amount of favorites attached to streams owned by the user */
+ totalOwnedStreamsFavorites: Scalars['Int'];
+ verified?: Maybe;
+};
+
+
+/**
+ * Limited user type, for showing public info about a user
+ * to another user
+ */
+export type LimitedUserActivityArgs = {
+ actionType?: InputMaybe;
+ after?: InputMaybe;
+ before?: InputMaybe;
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+/**
+ * Limited user type, for showing public info about a user
+ * to another user
+ */
+export type LimitedUserCommitsArgs = {
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+/**
+ * Limited user type, for showing public info about a user
+ * to another user
+ */
+export type LimitedUserStreamsArgs = {
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+/**
+ * Limited user type, for showing public info about a user
+ * to another user
+ */
+export type LimitedUserTimelineArgs = {
+ after?: InputMaybe;
+ before?: InputMaybe;
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+export type Model = {
+ __typename?: 'Model';
+ author: LimitedUser;
+ /** Return a model tree of children */
+ childrenTree: Array;
+ /** All comment threads in this model */
+ commentThreads: CommentCollection;
+ createdAt: Scalars['DateTime'];
+ description?: Maybe;
+ /** The shortened/display name that doesn't include the names of parent models */
+ displayName: Scalars['String'];
+ id: Scalars['ID'];
+ /** Full name including the names of parent models delimited by forward slashes */
+ name: Scalars['String'];
+ /** Returns a list of versions that are being created from a file import */
+ pendingImportedVersions: Array;
+ previewUrl?: Maybe;
+ updatedAt: Scalars['DateTime'];
+ version?: Maybe;
+ versions: VersionCollection;
+};
+
+
+export type ModelCommentThreadsArgs = {
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+export type ModelPendingImportedVersionsArgs = {
+ limit?: InputMaybe;
+};
+
+
+export type ModelVersionArgs = {
+ id: Scalars['String'];
+};
+
+
+export type ModelVersionsArgs = {
+ cursor?: InputMaybe;
+ filter?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+export type ModelCollection = {
+ __typename?: 'ModelCollection';
+ cursor?: Maybe;
+ items: Array;
+ totalCount: Scalars['Int'];
+};
+
+export type ModelMutations = {
+ __typename?: 'ModelMutations';
+ create: Model;
+ delete: Scalars['Boolean'];
+ update: Model;
+};
+
+
+export type ModelMutationsCreateArgs = {
+ input: CreateModelInput;
+};
+
+
+export type ModelMutationsDeleteArgs = {
+ input: DeleteModelInput;
+};
+
+
+export type ModelMutationsUpdateArgs = {
+ input: UpdateModelInput;
+};
+
+export type ModelVersionsFilter = {
+ /** Make sure these specified versions are always loaded first */
+ priorityIds?: InputMaybe>;
+};
+
+export type ModelsTreeItem = {
+ __typename?: 'ModelsTreeItem';
+ children: Array;
+ fullName: Scalars['String'];
+ /** Whether or not this item has nested children models */
+ hasChildren: Scalars['Boolean'];
+ id: Scalars['ID'];
+ /**
+ * Nullable cause the item can represent a parent that doesn't actually exist as a model on its own.
+ * E.g. A model named "foo/bar" is supposed to be a child of "foo" and will be represented as such,
+ * even if "foo" doesn't exist as its own model.
+ */
+ model?: Maybe;
+ name: Scalars['String'];
+ updatedAt: Scalars['DateTime'];
+};
+
+export type ModelsTreeItemCollection = {
+ __typename?: 'ModelsTreeItemCollection';
+ cursor?: Maybe;
+ items: Array;
+ totalCount: Scalars['Int'];
+};
+
+export type MoveVersionsInput = {
+ /** If the name references a nonexistant model, it will be created */
+ targetModelName: Scalars['String'];
+ versionIds: Array;
+};
+
+export type Mutation = {
+ __typename?: 'Mutation';
+ /** The void stares back. */
+ _?: Maybe;
+ /** Various Active User oriented mutations */
+ activeUserMutations: ActiveUserMutations;
+ adminDeleteUser: Scalars['Boolean'];
+ /** Creates an personal api token. */
+ apiTokenCreate: Scalars['String'];
+ /** Revokes (deletes) an personal api token. */
+ apiTokenRevoke: Scalars['Boolean'];
+ /** Register a new third party application. */
+ appCreate: Scalars['String'];
+ /** Deletes a thirty party application. */
+ appDelete: Scalars['Boolean'];
+ /** Revokes (de-authorizes) an application that you have previously authorized. */
+ appRevokeAccess?: Maybe;
+ /** Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.** */
+ appUpdate: Scalars['Boolean'];
+ branchCreate: Scalars['String'];
+ branchDelete: Scalars['Boolean'];
+ branchUpdate: Scalars['Boolean'];
+ /** Broadcast user activity in the viewer */
+ broadcastViewerUserActivity: Scalars['Boolean'];
+ /**
+ * Archives a comment.
+ * @deprecated Use commentMutations version
+ */
+ commentArchive: Scalars['Boolean'];
+ /**
+ * Creates a comment
+ * @deprecated Use commentMutations version
+ */
+ commentCreate: Scalars['String'];
+ /**
+ * Edits a comment.
+ * @deprecated Use commentMutations version
+ */
+ commentEdit: Scalars['Boolean'];
+ commentMutations: CommentMutations;
+ /**
+ * Adds a reply to a comment.
+ * @deprecated Use commentMutations version
+ */
+ commentReply: Scalars['String'];
+ /**
+ * Flags a comment as viewed by you (the logged in user).
+ * @deprecated Use commentMutations version
+ */
+ commentView: Scalars['Boolean'];
+ commitCreate: Scalars['String'];
+ commitDelete: Scalars['Boolean'];
+ commitReceive: Scalars['Boolean'];
+ commitUpdate: Scalars['Boolean'];
+ /** Delete a batch of commits */
+ commitsDelete: Scalars['Boolean'];
+ /** Move a batch of commits to a new branch */
+ commitsMove: Scalars['Boolean'];
+ /** Delete a pending invite */
+ inviteDelete: Scalars['Boolean'];
+ /** Re-send a pending invite */
+ inviteResend: Scalars['Boolean'];
+ modelMutations: ModelMutations;
+ objectCreate: Array>;
+ projectMutations: ProjectMutations;
+ /** (Re-)send the account verification e-mail */
+ requestVerification: Scalars['Boolean'];
+ serverInfoUpdate?: Maybe;
+ serverInviteBatchCreate: Scalars['Boolean'];
+ /** Invite a new user to the speckle server and return the invite ID */
+ serverInviteCreate: Scalars['Boolean'];
+ /** Request access to a specific stream */
+ streamAccessRequestCreate: StreamAccessRequest;
+ /** Accept or decline a stream access request. Must be a stream owner to invoke this. */
+ streamAccessRequestUse: Scalars['Boolean'];
+ /** Creates a new stream. */
+ streamCreate?: Maybe;
+ /** Deletes an existing stream. */
+ streamDelete: Scalars['Boolean'];
+ streamFavorite?: Maybe;
+ streamInviteBatchCreate: Scalars['Boolean'];
+ /** Cancel a pending stream invite. Can only be invoked by a stream owner. */
+ streamInviteCancel: Scalars['Boolean'];
+ /** Invite a new or registered user to the specified stream */
+ streamInviteCreate: Scalars['Boolean'];
+ /** Accept or decline a stream invite */
+ streamInviteUse: Scalars['Boolean'];
+ /** Remove yourself from stream collaborators (not possible for the owner) */
+ streamLeave: Scalars['Boolean'];
+ /** Revokes the permissions of a user on a given stream. */
+ streamRevokePermission?: Maybe;
+ /** Updates an existing stream. */
+ streamUpdate: Scalars['Boolean'];
+ /** Update permissions of a user on a given stream. */
+ streamUpdatePermission?: Maybe;
+ streamsDelete: Scalars['Boolean'];
+ /**
+ * Used for broadcasting real time typing status in comment threads. Does not persist any info.
+ * @deprecated Use broadcastViewerUserActivity
+ */
+ userCommentThreadActivityBroadcast: Scalars['Boolean'];
+ /** Delete a user's account. */
+ userDelete: Scalars['Boolean'];
+ userNotificationPreferencesUpdate?: Maybe;
+ userRoleChange: Scalars['Boolean'];
+ /**
+ * Edits a user's profile.
+ * @deprecated Use activeUserMutations version
+ */
+ userUpdate: Scalars['Boolean'];
+ /**
+ * Used for broadcasting real time chat head bubbles and status. Does not persist any info.
+ * @deprecated Use broadcastViewerUserActivity
+ */
+ userViewerActivityBroadcast: Scalars['Boolean'];
+ versionMutations: VersionMutations;
+ /** Creates a new webhook on a stream */
+ webhookCreate: Scalars['String'];
+ /** Deletes an existing webhook */
+ webhookDelete: Scalars['String'];
+ /** Updates an existing webhook */
+ webhookUpdate: Scalars['String'];
+};
+
+
+export type MutationAdminDeleteUserArgs = {
+ userConfirmation: UserDeleteInput;
+};
+
+
+export type MutationApiTokenCreateArgs = {
+ token: ApiTokenCreateInput;
+};
+
+
+export type MutationApiTokenRevokeArgs = {
+ token: Scalars['String'];
+};
+
+
+export type MutationAppCreateArgs = {
+ app: AppCreateInput;
+};
+
+
+export type MutationAppDeleteArgs = {
+ appId: Scalars['String'];
+};
+
+
+export type MutationAppRevokeAccessArgs = {
+ appId: Scalars['String'];
+};
+
+
+export type MutationAppUpdateArgs = {
+ app: AppUpdateInput;
+};
+
+
+export type MutationBranchCreateArgs = {
+ branch: BranchCreateInput;
+};
+
+
+export type MutationBranchDeleteArgs = {
+ branch: BranchDeleteInput;
+};
+
+
+export type MutationBranchUpdateArgs = {
+ branch: BranchUpdateInput;
+};
+
+
+export type MutationBroadcastViewerUserActivityArgs = {
+ message: ViewerUserActivityMessageInput;
+ projectId: Scalars['String'];
+ resourceIdString: Scalars['String'];
+};
+
+
+export type MutationCommentArchiveArgs = {
+ archived?: Scalars['Boolean'];
+ commentId: Scalars['String'];
+ streamId: Scalars['String'];
+};
+
+
+export type MutationCommentCreateArgs = {
+ input: CommentCreateInput;
+};
+
+
+export type MutationCommentEditArgs = {
+ input: CommentEditInput;
+};
+
+
+export type MutationCommentReplyArgs = {
+ input: ReplyCreateInput;
+};
+
+
+export type MutationCommentViewArgs = {
+ commentId: Scalars['String'];
+ streamId: Scalars['String'];
+};
+
+
+export type MutationCommitCreateArgs = {
+ commit: CommitCreateInput;
+};
+
+
+export type MutationCommitDeleteArgs = {
+ commit: CommitDeleteInput;
+};
+
+
+export type MutationCommitReceiveArgs = {
+ input: CommitReceivedInput;
+};
+
+
+export type MutationCommitUpdateArgs = {
+ commit: CommitUpdateInput;
+};
+
+
+export type MutationCommitsDeleteArgs = {
+ input: CommitsDeleteInput;
+};
+
+
+export type MutationCommitsMoveArgs = {
+ input: CommitsMoveInput;
+};
+
+
+export type MutationInviteDeleteArgs = {
+ inviteId: Scalars['String'];
+};
+
+
+export type MutationInviteResendArgs = {
+ inviteId: Scalars['String'];
+};
+
+
+export type MutationObjectCreateArgs = {
+ objectInput: ObjectCreateInput;
+};
+
+
+export type MutationServerInfoUpdateArgs = {
+ info: ServerInfoUpdateInput;
+};
+
+
+export type MutationServerInviteBatchCreateArgs = {
+ input: Array;
+};
+
+
+export type MutationServerInviteCreateArgs = {
+ input: ServerInviteCreateInput;
+};
+
+
+export type MutationStreamAccessRequestCreateArgs = {
+ streamId: Scalars['String'];
+};
+
+
+export type MutationStreamAccessRequestUseArgs = {
+ accept: Scalars['Boolean'];
+ requestId: Scalars['String'];
+ role?: StreamRole;
+};
+
+
+export type MutationStreamCreateArgs = {
+ stream: StreamCreateInput;
+};
+
+
+export type MutationStreamDeleteArgs = {
+ id: Scalars['String'];
+};
+
+
+export type MutationStreamFavoriteArgs = {
+ favorited: Scalars['Boolean'];
+ streamId: Scalars['String'];
+};
+
+
+export type MutationStreamInviteBatchCreateArgs = {
+ input: Array;
+};
+
+
+export type MutationStreamInviteCancelArgs = {
+ inviteId: Scalars['String'];
+ streamId: Scalars['String'];
+};
+
+
+export type MutationStreamInviteCreateArgs = {
+ input: StreamInviteCreateInput;
+};
+
+
+export type MutationStreamInviteUseArgs = {
+ accept: Scalars['Boolean'];
+ streamId: Scalars['String'];
+ token: Scalars['String'];
+};
+
+
+export type MutationStreamLeaveArgs = {
+ streamId: Scalars['String'];
+};
+
+
+export type MutationStreamRevokePermissionArgs = {
+ permissionParams: StreamRevokePermissionInput;
+};
+
+
+export type MutationStreamUpdateArgs = {
+ stream: StreamUpdateInput;
+};
+
+
+export type MutationStreamUpdatePermissionArgs = {
+ permissionParams: StreamUpdatePermissionInput;
+};
+
+
+export type MutationStreamsDeleteArgs = {
+ ids?: InputMaybe>;
+};
+
+
+export type MutationUserCommentThreadActivityBroadcastArgs = {
+ commentId: Scalars['String'];
+ data?: InputMaybe;
+ streamId: Scalars['String'];
+};
+
+
+export type MutationUserDeleteArgs = {
+ userConfirmation: UserDeleteInput;
+};
+
+
+export type MutationUserNotificationPreferencesUpdateArgs = {
+ preferences: Scalars['JSONObject'];
+};
+
+
+export type MutationUserRoleChangeArgs = {
+ userRoleInput: UserRoleInput;
+};
+
+
+export type MutationUserUpdateArgs = {
+ user: UserUpdateInput;
+};
+
+
+export type MutationUserViewerActivityBroadcastArgs = {
+ data?: InputMaybe;
+ resourceId: Scalars['String'];
+ streamId: Scalars['String'];
+};
+
+
+export type MutationWebhookCreateArgs = {
+ webhook: WebhookCreateInput;
+};
+
+
+export type MutationWebhookDeleteArgs = {
+ webhook: WebhookDeleteInput;
+};
+
+
+export type MutationWebhookUpdateArgs = {
+ webhook: WebhookUpdateInput;
+};
+
+export type Object = {
+ __typename?: 'Object';
+ applicationId?: Maybe;
+ /**
+ * Get any objects that this object references. In the case of commits, this will give you a commit's constituent objects.
+ * **NOTE**: Providing any of the two last arguments ( `query`, `orderBy` ) will trigger a different code branch that executes a much more expensive SQL query. It is not recommended to do so for basic clients that are interested in purely getting all the objects of a given commit.
+ */
+ children: ObjectCollection;
+ /**
+ * The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this object's id.
+ * E.g.,
+ * ```
+ * query{
+ * comments(streamId:"streamId" resources:[{resourceType: object, resourceId:"objectId"}] ){
+ * ...
+ * }
+ * ```
+ */
+ commentCount: Scalars['Int'];
+ createdAt?: Maybe;
+ /** The full object, with all its props & other things. **NOTE:** If you're requesting objects for the purpose of recreating & displaying, you probably only want to request this specific field. */
+ data?: Maybe;
+ id: Scalars['String'];
+ speckleType?: Maybe;
+ totalChildrenCount?: Maybe;
+};
+
+
+export type ObjectChildrenArgs = {
+ cursor?: InputMaybe;
+ depth?: Scalars['Int'];
+ limit?: Scalars['Int'];
+ orderBy?: InputMaybe;
+ query?: InputMaybe>;
+ select?: InputMaybe>>;
+};
+
+export type ObjectCollection = {
+ __typename?: 'ObjectCollection';
+ cursor?: Maybe;
+ objects: Array>;
+ totalCount: Scalars['Int'];
+};
+
+export type ObjectCreateInput = {
+ /** The objects you want to create. */
+ objects: Array>;
+ /** The stream against which these objects will be created. */
+ streamId: Scalars['String'];
+};
+
+export type PasswordStrengthCheckFeedback = {
+ __typename?: 'PasswordStrengthCheckFeedback';
+ suggestions: Array;
+ warning?: Maybe;
+};
+
+export type PasswordStrengthCheckResults = {
+ __typename?: 'PasswordStrengthCheckResults';
+ /** Verbal feedback to help choose better passwords. set when score <= 2. */
+ feedback: PasswordStrengthCheckFeedback;
+ /**
+ * Integer from 0-4 (useful for implementing a strength bar):
+ * 0 too guessable: risky password. (guesses < 10^3)
+ * 1 very guessable: protection from throttled online attacks. (guesses < 10^6)
+ * 2 somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
+ * 3 safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
+ * 4 very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
+ */
+ score: Scalars['Int'];
+};
+
+export type PendingStreamCollaborator = {
+ __typename?: 'PendingStreamCollaborator';
+ id: Scalars['String'];
+ inviteId: Scalars['String'];
+ invitedBy: LimitedUser;
+ projectId: Scalars['String'];
+ projectName: Scalars['String'];
+ role: Scalars['String'];
+ streamId: Scalars['String'];
+ streamName: Scalars['String'];
+ /** E-mail address or name of the invited user */
+ title: Scalars['String'];
+ /** Only available if the active user is the pending stream collaborator */
+ token?: Maybe;
+ /** Set only if user is registered */
+ user?: Maybe;
+};
+
+export type Project = {
+ __typename?: 'Project';
+ allowPublicComments: Scalars['Boolean'];
+ /** All comment threads in this project */
+ commentThreads: ProjectCommentCollection;
+ createdAt: Scalars['DateTime'];
+ description?: Maybe;
+ id: Scalars['ID'];
+ /** Collaborators who have been invited, but not yet accepted. */
+ invitedTeam?: Maybe>;
+ /** Returns a specific model by its ID */
+ model?: Maybe;
+ /** Return a model tree of children for the specified model name */
+ modelChildrenTree: Array;
+ /** Returns a flat list of all models */
+ models: ModelCollection;
+ /**
+ * Return's a project's models in a tree view with submodels being nested under parent models
+ * real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
+ */
+ modelsTree: ModelsTreeItemCollection;
+ name: Scalars['String'];
+ /** Returns a list models that are being created from a file import */
+ pendingImportedModels: Array;
+ /** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */
+ role?: Maybe;
+ /** Source apps used in any models of this project */
+ sourceApps: Array;
+ team: Array;
+ updatedAt: Scalars['DateTime'];
+ /** Returns a flat list of all project versions */
+ versions: VersionCollection;
+ /** Return metadata about resources being requested in the viewer */
+ viewerResources: Array;
+ visibility: ProjectVisibility;
+};
+
+
+export type ProjectCommentThreadsArgs = {
+ cursor?: InputMaybe;
+ filter?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+export type ProjectModelArgs = {
+ id: Scalars['String'];
+};
+
+
+export type ProjectModelChildrenTreeArgs = {
+ fullName: Scalars['String'];
+};
+
+
+export type ProjectModelsArgs = {
+ cursor?: InputMaybe;
+ filter?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+export type ProjectModelsTreeArgs = {
+ cursor?: InputMaybe;
+ filter?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+export type ProjectPendingImportedModelsArgs = {
+ limit?: InputMaybe;
+};
+
+
+export type ProjectVersionsArgs = {
+ cursor?: InputMaybe;
+ limit?: Scalars['Int'];
+};
+
+
+export type ProjectViewerResourcesArgs = {
+ loadedVersionsOnly?: InputMaybe;
+ resourceIdString: Scalars['String'];
+};
+
+export type ProjectCollaborator = {
+ __typename?: 'ProjectCollaborator';
+ role: Scalars['String'];
+ user: LimitedUser;
+};
+
+export type ProjectCollection = {
+ __typename?: 'ProjectCollection';
+ cursor?: Maybe;
+ items: Array;
+ totalCount: Scalars['Int'];
+};
+
+export type ProjectCommentCollection = {
+ __typename?: 'ProjectCommentCollection';
+ cursor?: Maybe;
+ items: Array]