feat(frontend): comments in viewer embed + refactored frontend viewer foundations
This commit is contained in:
@@ -6,8 +6,6 @@
|
||||
|
||||
We're working to stabilize the 2.0 API, and until then there will be breaking changes.
|
||||
|
||||
Note that this package contains two vue apps, the main frontend (located under @/main), and the viewer embed app (@/embed).
|
||||
|
||||
Notes:
|
||||
|
||||
- In **development** mode, the Speckle Server will proxy the frontend from `localhost:3000` to `localhost:8080`. If you don't see anything, ensure you've run `yarn serve` in the frontend package.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
overwrite: true
|
||||
schema: 'http://localhost:3000/graphql'
|
||||
schema:
|
||||
- 'http://localhost:3000/graphql'
|
||||
- 'src/graphql/local-only-schema/schema.gql'
|
||||
documents:
|
||||
- 'src/graphql/**/*.gql'
|
||||
- 'src/**/*.{ts,tsx,js,jsx,vue}'
|
||||
@@ -13,3 +15,4 @@ generates:
|
||||
config:
|
||||
scalars:
|
||||
JSONObject: Record<string, unknown>
|
||||
UnknownLocalObject: unknown
|
||||
|
||||
@@ -82,12 +82,6 @@ server {
|
||||
expires 1y;
|
||||
}
|
||||
|
||||
location /embed {
|
||||
default_type text/html;
|
||||
alias /usr/share/nginx/html/embedApp.html;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
location ~ ^/streams/.* {
|
||||
default_type text/html;
|
||||
content_by_lua_block {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "2.5.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "ws -p 8080 -d dist -r '/embed(.*) -> /embedApp.html' '/([a-zA-Z0-9-_/]*)(\\?.*)? -> /app.html' ",
|
||||
"serve": "ws -p 8080 -d dist -r '/([a-zA-Z0-9-_/]*)(\\?.*)? -> /app.html' ",
|
||||
"build": "vue-cli-service build --mode production --silent",
|
||||
"lint": "eslint . --ext .js,.ts,.vue,.tsx,.jsx",
|
||||
"lint:vue": "vti diagnostics",
|
||||
@@ -38,6 +38,7 @@
|
||||
"@vue/apollo-option": "^4.0.0-alpha.20",
|
||||
"@vuejs-community/vue-filter-date-format": "^1.6.3",
|
||||
"@vuejs-community/vue-filter-date-parse": "^1.1.6",
|
||||
"@vueuse/core": "^9.0.0",
|
||||
"apexcharts": "^3.33.1",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
"dompurify": "^2.3.6",
|
||||
@@ -61,8 +62,7 @@
|
||||
"vue2-perfect-scrollbar": "^1.5.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.3.21",
|
||||
"vuetify-image-input": "^19.1.0",
|
||||
"vuex": "^3.6.2"
|
||||
"vuetify-image-input": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.6.2",
|
||||
@@ -84,7 +84,6 @@
|
||||
"@vue/cli-plugin-babel": "~4.3.1",
|
||||
"@vue/cli-plugin-router": "~4.3.1",
|
||||
"@vue/cli-plugin-typescript": "~4.5.19",
|
||||
"@vue/cli-plugin-vuex": "~4.3.1",
|
||||
"@vue/cli-service": "~4.3.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- <base href="/appname/"> -->
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"
|
||||
/>
|
||||
<style type="text/css">
|
||||
/* body {
|
||||
background-color: #333333;
|
||||
color: #0a66ff;
|
||||
}
|
||||
|
||||
@media screen and (prefers-color-scheme: light) {
|
||||
body {
|
||||
background-color: white;
|
||||
color: #0a66ff;
|
||||
}
|
||||
} */
|
||||
.hover-tada:hover {
|
||||
-webkit-animation-name: tada;
|
||||
animation-name: tada;
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.tada {
|
||||
-webkit-animation-name: tada;
|
||||
animation-name: tada;
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes tada {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
|
||||
10%,
|
||||
20% {
|
||||
-webkit-transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tada {
|
||||
0% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
|
||||
10%,
|
||||
20% {
|
||||
-webkit-transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
-webkit-transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
transform: scale3d(1.4, 1.4, 1.4) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>
|
||||
We're sorry but Speckle doesn't work properly without JavaScript enabled. Please
|
||||
enable it to continue.
|
||||
</strong>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div
|
||||
style="
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
font-family: sans-serif !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
"
|
||||
>
|
||||
<img src="<%= BASE_URL %>logo.svg" style="max-width: 50px" class="tada" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,25 +5,13 @@ import PortalVue from 'portal-vue'
|
||||
import { formatNumber } from '@/plugins/formatNumber'
|
||||
|
||||
/**
|
||||
* Global bootstrapping that is used in all of the frontend apps (main/embed)
|
||||
* Global bootstrapping for the frontend app
|
||||
*/
|
||||
|
||||
// Filter to turn any number into a nice string like '10k', '5.5m'
|
||||
// Accepts 'max' parameter to set it's formatting while being animated
|
||||
Vue.filter('prettynum', formatNumber)
|
||||
|
||||
// Async HistogramSlider load
|
||||
// TODO: Instead of bundling it globally on all pages, only import it where needed
|
||||
Vue.component('HistogramSlider', async () => {
|
||||
await import(
|
||||
/* webpackChunkName: "vue-histogram-slider" */ 'vue-histogram-slider/dist/histogram-slider.css'
|
||||
)
|
||||
const component = await import(
|
||||
/* webpackChunkName: "vue-histogram-slider" */ 'vue-histogram-slider'
|
||||
)
|
||||
return component
|
||||
})
|
||||
|
||||
// process.env.NODE_ENV is injected by Webpack
|
||||
Vue.config.productionTip = process.env.NODE_ENV === 'development'
|
||||
|
||||
@@ -33,6 +21,7 @@ Vue.use(VTooltip, {
|
||||
defaultHtml: false
|
||||
})
|
||||
|
||||
// In highly restrictive sandboxed environments mixpanel init might fail due to document.cookie access
|
||||
Vue.use(VueMixpanel, {
|
||||
token: 'acd87c5a50b56df91a795e999812a3a4',
|
||||
config: {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import Vue from 'vue'
|
||||
import { createApolloProvider, ApolloProvider } from '@vue/apollo-option'
|
||||
import { ApolloClient, ApolloLink, InMemoryCache, split } from '@apollo/client/core'
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloLink,
|
||||
InMemoryCache,
|
||||
split,
|
||||
TypePolicies
|
||||
} from '@apollo/client/core'
|
||||
import { setContext } from '@apollo/client/link/context'
|
||||
import { WebSocketLink } from '@apollo/client/link/ws'
|
||||
import { SubscriptionClient } from 'subscriptions-transport-ws'
|
||||
@@ -13,6 +19,8 @@ import {
|
||||
buildAbstractCollectionMergeFunction,
|
||||
incomingOverwritesExistingMergeFunction
|
||||
} from '@/main/lib/core/helpers/apolloSetupHelper'
|
||||
import { merge } from 'lodash'
|
||||
import { statePolicies as commitObjectViewerStatePolicies } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
// Name of the localStorage item
|
||||
const AUTH_TOKEN = LocalStorageKeys.AuthToken
|
||||
@@ -38,88 +46,101 @@ function createCache(): InMemoryCache {
|
||||
*
|
||||
* Read more: https://www.apollographql.com/docs/react/caching/cache-field-behavior
|
||||
*/
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
user: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'User', id: args.id })
|
||||
}
|
||||
typePolicies: merge<TypePolicies, TypePolicies>(
|
||||
{
|
||||
Query: {
|
||||
fields: {
|
||||
user: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'User', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
}
|
||||
},
|
||||
stream: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'Stream', id: args.id })
|
||||
return original
|
||||
}
|
||||
},
|
||||
stream: {
|
||||
read(original, { args, toReference }) {
|
||||
if (args?.id) {
|
||||
return toReference({ __typename: 'Stream', id: args.id })
|
||||
}
|
||||
|
||||
return original
|
||||
return original
|
||||
}
|
||||
},
|
||||
streams: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
}
|
||||
},
|
||||
streams: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection')
|
||||
}
|
||||
},
|
||||
User: {
|
||||
fields: {
|
||||
timeline: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
||||
},
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollectionUser', {
|
||||
checkIdentity: true
|
||||
})
|
||||
},
|
||||
favoriteStreams: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
Stream: {
|
||||
fields: {
|
||||
activity: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
||||
},
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
},
|
||||
pendingCollaborators: {
|
||||
merge: incomingOverwritesExistingMergeFunction
|
||||
}
|
||||
}
|
||||
},
|
||||
Branch: {
|
||||
fields: {
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection', {
|
||||
checkIdentity: true
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
BranchCollection: {
|
||||
merge: true
|
||||
},
|
||||
ServerStats: {
|
||||
merge: true
|
||||
},
|
||||
WebhookEventCollection: {
|
||||
merge: true
|
||||
},
|
||||
ServerInfo: {
|
||||
merge: true
|
||||
},
|
||||
CommentThreadActivityMessage: {
|
||||
merge: true
|
||||
}
|
||||
},
|
||||
User: {
|
||||
fields: {
|
||||
timeline: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
||||
},
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollectionUser')
|
||||
},
|
||||
favoriteStreams: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('StreamCollection')
|
||||
}
|
||||
}
|
||||
},
|
||||
Stream: {
|
||||
fields: {
|
||||
activity: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('ActivityCollection')
|
||||
},
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection')
|
||||
},
|
||||
pendingCollaborators: {
|
||||
merge: incomingOverwritesExistingMergeFunction
|
||||
}
|
||||
}
|
||||
},
|
||||
Branch: {
|
||||
fields: {
|
||||
commits: {
|
||||
keyArgs: false,
|
||||
merge: buildAbstractCollectionMergeFunction('CommitCollection')
|
||||
}
|
||||
}
|
||||
},
|
||||
BranchCollection: {
|
||||
merge: true
|
||||
},
|
||||
ServerStats: {
|
||||
merge: true
|
||||
},
|
||||
WebhookEventCollection: {
|
||||
merge: true
|
||||
},
|
||||
ServerInfo: {
|
||||
merge: true
|
||||
},
|
||||
CommentThreadActivityMessage: {
|
||||
merge: true
|
||||
}
|
||||
}
|
||||
commitObjectViewerStatePolicies
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<template lang="html">
|
||||
<router-view />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,269 +0,0 @@
|
||||
<template>
|
||||
<v-app
|
||||
:class="`embed-viewer no-scrollbar ${
|
||||
$route.query.transparent === 'true'
|
||||
? ''
|
||||
: $vuetify.theme.dark
|
||||
? 'background-dark'
|
||||
: 'background-light'
|
||||
}`"
|
||||
>
|
||||
<!-- BG image -->
|
||||
<div
|
||||
v-if="objectIdsToLoad.length !== 0 && !isModelLoaded"
|
||||
style="position: fixed; top: 0; width: 100%; height: 100%; cursor: pointer"
|
||||
@click="load()"
|
||||
>
|
||||
<preview-image
|
||||
:url="`/preview/${$route.query.stream}/objects/${objectIdsToLoad[0]}`"
|
||||
:height="height"
|
||||
rotate
|
||||
></preview-image>
|
||||
</div>
|
||||
|
||||
<!-- Play button -->
|
||||
<div
|
||||
v-if="!isModelLoaded && !error"
|
||||
class="viewer-play d-flex fullscreen align-center justify-center no-mouse"
|
||||
>
|
||||
<v-btn
|
||||
id="viewer-play-btn"
|
||||
:disabled="showPlayLoader"
|
||||
fab
|
||||
color="primary"
|
||||
class="elevation-4 hover-tada mouse"
|
||||
@click="load()"
|
||||
>
|
||||
<v-icon v-if="!showPlayLoader">mdi-play</v-icon>
|
||||
<v-icon v-else class="spinning-icon">mdi-loading</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Async loaded viewer -->
|
||||
<embed-viewer-core
|
||||
v-if="shouldLoadHeavyDeps"
|
||||
:objects="objectIdsToLoad"
|
||||
@model-loaded="onModelLoaded"
|
||||
@error="onError"
|
||||
/>
|
||||
|
||||
<!-- Display error if needed -->
|
||||
<div v-if="error" class="fullscreen d-flex justify-center align-center">
|
||||
<div class="">
|
||||
<p class="text-h5 text-center red--text">Speckle Embedding Error</p>
|
||||
<p class="text-center grey--text">
|
||||
Double check to see if the stream is public and if the embed link is correct.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Nullable } from '@/helpers/typeHelpers'
|
||||
import { getStreamObj, getBranchObj, getCommitObj } from '@/embed/speckleUtils'
|
||||
import Vue from 'vue'
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - Move EmbedViewer back to main app? The main app has a lot of global dependencies
|
||||
* that this endpoint doesn't need, tho...
|
||||
* - Make speckle-viewer configurable through props/events not through window.__viewer
|
||||
*/
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'EmbedViewer',
|
||||
components: {
|
||||
EmbedViewerCore: () => import('@/embed/EmbedViewerCore.vue'),
|
||||
PreviewImage: () => import('@/main/components/common/PreviewImage.vue')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isModelLoaded: false,
|
||||
error: null as Nullable<Error>,
|
||||
displayType: 'stream',
|
||||
objectIdsToLoad: [] as any[],
|
||||
input: {
|
||||
stream: this.$route.query.stream,
|
||||
object: this.$route.query.object,
|
||||
branch: this.$route.query.branch || 'main',
|
||||
commit: this.$route.query.commit,
|
||||
overlay: this.$route.query.overlay,
|
||||
camera: this.$route.query.c,
|
||||
filter: this.$route.query.filter
|
||||
} as Record<string, string>,
|
||||
isInitialized: false as boolean,
|
||||
shouldLoadHeavyDeps: false as boolean,
|
||||
height: window.innerHeight
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
streamId(): string {
|
||||
return this.$route.query.stream as string
|
||||
},
|
||||
objectUrl(): string {
|
||||
return `${window.location.protocol}//${window.location.host}/streams/${this.input.stream}/objects/${this.input.object}`
|
||||
},
|
||||
showPlayLoader(): boolean {
|
||||
return !this.isInitialized || (this.shouldLoadHeavyDeps && !this.isModelLoaded)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$route.query.transparent === 'true') {
|
||||
document.getElementById('app')!.classList.remove('theme--dark')
|
||||
document.getElementById('app')!.classList.remove('theme--light')
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
this.height = window.innerHeight
|
||||
})
|
||||
},
|
||||
async beforeMount() {
|
||||
if (this.$route.query.stream) this.displayType = 'stream'
|
||||
if (this.$route.query.branch) this.displayType = 'branch'
|
||||
if (this.$route.query.commit) this.displayType = 'commit'
|
||||
if (this.$route.query.object) this.displayType = 'object'
|
||||
if (this.$route.query.overlay) this.displayType = 'multiple'
|
||||
|
||||
try {
|
||||
switch (this.displayType) {
|
||||
case 'stream': {
|
||||
const res = await getStreamObj(this.$route.query.stream)
|
||||
if (res.data.stream.commits.totalCount === 0)
|
||||
throw new Error('Stream has no commits.')
|
||||
this.objectIdsToLoad.push(res.data.stream.commits.items[0].referencedObject)
|
||||
break
|
||||
}
|
||||
case 'branch': {
|
||||
const res = await getBranchObj(
|
||||
this.$route.query.stream,
|
||||
this.$route.query.branch
|
||||
)
|
||||
if (res.data.stream.branch.commits.totalCount === 0)
|
||||
throw new Error('Branch has no commits.')
|
||||
this.objectIdsToLoad.push(
|
||||
res.data.stream.branch.commits.items[0].referencedObject
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'commit': {
|
||||
const res = await getCommitObj(
|
||||
this.$route.query.stream,
|
||||
this.$route.query.commit
|
||||
)
|
||||
this.objectIdsToLoad.push(res.data.stream.commit.referencedObject)
|
||||
break
|
||||
}
|
||||
case 'object':
|
||||
this.objectIdsToLoad.push(this.$route.query.object)
|
||||
break
|
||||
case 'multiple': {
|
||||
if (this.$route.query.commit) {
|
||||
const res = await getCommitObj(
|
||||
this.$route.query.stream,
|
||||
this.$route.query.commit
|
||||
)
|
||||
this.objectIdsToLoad.push(res.data.stream.commit.referencedObject)
|
||||
} else {
|
||||
this.objectIdsToLoad.push(this.$route.query.object)
|
||||
}
|
||||
for (const resId of (this.$route.query.overlay as string).split(',')) {
|
||||
if (resId.length === 10) {
|
||||
const res = await getCommitObj(this.$route.query.stream, resId)
|
||||
this.objectIdsToLoad.push(res.data.stream.commit.referencedObject)
|
||||
} else {
|
||||
this.objectIdsToLoad.push(resId)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Mark as initialized (enable play button)
|
||||
this.isInitialized = true
|
||||
} catch (e: unknown) {
|
||||
this.error = e instanceof Error ? e : new Error('Unexpected error')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onError(e: unknown) {
|
||||
this.error = e instanceof Error ? e : new Error('Unexpected error')
|
||||
},
|
||||
onModelLoaded() {
|
||||
this.isModelLoaded = true
|
||||
},
|
||||
load() {
|
||||
if (!this.isInitialized || this.shouldLoadHeavyDeps || this.isModelLoaded) return
|
||||
|
||||
this.shouldLoadHeavyDeps = true
|
||||
this.$mixpanel.track('Embedded Model Load', {
|
||||
type: 'action'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
position: fixed;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-img {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/*background-attachment: fixed;*/
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
#viewer-play-btn {
|
||||
@keyframes spinner-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// kinda hacky, but vuetify renders the disabled state in a stupid way that relies
|
||||
// on the background color of the thing behind the button
|
||||
&.v-btn--fab.v-btn--disabled {
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
|
||||
.spinning-icon {
|
||||
animation: spinner-spin 0.5s linear infinite;
|
||||
}
|
||||
}
|
||||
.no-mouse {
|
||||
pointer-events: none;
|
||||
}
|
||||
.mouse {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,234 +0,0 @@
|
||||
<template>
|
||||
<div class="embed-viewer-core">
|
||||
<!-- Viewer navbar (position fixed) -->
|
||||
<div v-if="!error" style="z-index: 100">
|
||||
<div
|
||||
class="top-left bottom-left pa-4 d-flex justify-space-between"
|
||||
style="right: 0px; position: fixed; z-index: 1000; width: 100%"
|
||||
>
|
||||
<v-btn fab small style="z-index=1000" @click="drawer = !drawer">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<span v-show="!drawer" class="caption d-inline-flex align-center">
|
||||
<img src="@/assets/logo.svg" height="18" />
|
||||
<span style="margin-top: 2px" class="primary--text">
|
||||
<a href="https://speckle.xyz" target="_blank" class="text-decoration-none">
|
||||
<b>Powered by Speckle</b>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-show="!drawer && loadedModel"
|
||||
class="caption grey--text pa-2"
|
||||
style="z-index=1000"
|
||||
></div>
|
||||
<div
|
||||
class="pa-2 d-flex align-center justify-space-between caption"
|
||||
style="position: fixed; bottom: 0; width: 100%"
|
||||
>
|
||||
<portal to="viewercontrols">
|
||||
<v-btn
|
||||
v-tooltip="'View extra details in Speckle!'"
|
||||
icon
|
||||
dark
|
||||
large
|
||||
class="elevation-5 primary pa-0 ma-o"
|
||||
:href="goToServerUrl"
|
||||
target="blank"
|
||||
>
|
||||
<v-icon dark small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</portal>
|
||||
</div>
|
||||
<div
|
||||
:style="`width: 100%; bottom: 12px; left: 0px; position: ${
|
||||
$isMobile() ? 'fixed' : 'absolute'
|
||||
}; z-index: 20`"
|
||||
:class="`d-flex justify-center`"
|
||||
>
|
||||
<viewer-controls v-show="loadedModel" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model loading progress bar -->
|
||||
<div
|
||||
v-if="!loadedModel && loadProgress > 0"
|
||||
class="viewer-loader d-flex fullscreen align-center justify-center"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-model="loadProgress"
|
||||
:indeterminate="loadProgress >= 99 && !loadedModel"
|
||||
color="primary"
|
||||
style="max-width: 30%"
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
|
||||
<!-- Viewer filters panel / sidebar -->
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
class="viewer-controls-drawer"
|
||||
app
|
||||
floating
|
||||
disable-resize-watcher
|
||||
style="z-index: 10000"
|
||||
>
|
||||
<div class="px-1 pt-1 d-flex flex-column" style="height: 100%; width: 100%">
|
||||
<!-- Drawer closer -->
|
||||
<v-btn icon small class="align-self-end mb-2" @click="drawer = false">
|
||||
<v-icon x-small>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- Views display -->
|
||||
<views-display v-if="views.length !== 0" :views="views" :sticky-top="false" />
|
||||
|
||||
<!-- Filters display -->
|
||||
<viewer-filters
|
||||
:props="objectProperties"
|
||||
style="width: 100%"
|
||||
:sticky-top="false"
|
||||
/>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- Actual viewer -->
|
||||
<div style="position: fixed" class="viewer-wrapper no-scrollbar fullscreen">
|
||||
<speckle-viewer @load-progress="captureProgress" @viewer-init="onViewerInit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Nullable } from '@/helpers/typeHelpers'
|
||||
import Vue from 'vue'
|
||||
import SpeckleViewer from '@/main/components/common/SpeckleViewer.vue'
|
||||
import ViewerControls from '@/main/components/viewer/ViewerControls.vue'
|
||||
import ViewsDisplay from '@/main/components/viewer/ViewsDisplay.vue'
|
||||
import ViewerFilters from '@/main/components/viewer/ViewerFilters.vue'
|
||||
|
||||
/**
|
||||
* Core embed viewer functionality with all of the heavy JS dependencies has been extracted to this component,
|
||||
* so that we can lazy-load it only when the user clicks on the "play" button
|
||||
*
|
||||
* Make sure this component isn't initialized until it's actually needed and don't put any heavy deps
|
||||
* inside EmbedViewer.vue (or asynchronize them)
|
||||
*/
|
||||
|
||||
type UnknownObject = Record<string, unknown>
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'EmbedViewerCore',
|
||||
components: {
|
||||
SpeckleViewer,
|
||||
ViewerControls,
|
||||
ViewsDisplay,
|
||||
ViewerFilters
|
||||
},
|
||||
props: {
|
||||
objects: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null as Nullable<Error>,
|
||||
drawer: false,
|
||||
loadedModel: false,
|
||||
loadProgress: 0,
|
||||
views: [] as Array<UnknownObject>,
|
||||
objectProperties: null as Nullable<UnknownObject>
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
goToServerUrl(): string {
|
||||
const base = `${window.location.origin}/streams/${this.$route.query.stream}/`
|
||||
|
||||
if (this.$route.query.commit) return base + `commits/${this.$route.query.commit}`
|
||||
|
||||
if (this.$route.query.object) return base + `objects/${this.$route.query.object}`
|
||||
|
||||
if (this.$route.query.branch)
|
||||
return base + `branches/${encodeURI(this.$route.query.branch as string)}`
|
||||
|
||||
return base
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
error(newVal: Error | null) {
|
||||
if (newVal) this.$emit('error', newVal)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
captureProgress(args: { progress: number; id: number; url: string }) {
|
||||
this.loadProgress = args.progress * 100
|
||||
},
|
||||
markModelLoaded() {
|
||||
this.loadedModel = true
|
||||
this.$emit('model-loaded')
|
||||
},
|
||||
async onViewerInit() {
|
||||
if (!window.__viewer) {
|
||||
throw new Error('Viewer instance unavailable')
|
||||
}
|
||||
|
||||
for (const id of this.objects)
|
||||
await window.__viewer.loadObject(
|
||||
`${window.location.origin}/streams/${this.$route.query.stream}/objects/${id}`
|
||||
)
|
||||
|
||||
window.__viewer.zoomExtents(undefined, true)
|
||||
|
||||
this.markModelLoaded()
|
||||
|
||||
this.views.push(...window.__viewer.sceneManager.views)
|
||||
this.objectProperties = await window.__viewer.getObjectsProperties()
|
||||
|
||||
if (this.$route.query.filter) {
|
||||
const parsedFilter = JSON.parse(this.$route.query.filter as string)
|
||||
setTimeout(() => {
|
||||
this.$store.commit('setFilterDirect', { filter: parsedFilter })
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (this.$route.query.c) {
|
||||
const cam = JSON.parse(this.$route.query.c as string)
|
||||
window.__viewer.interactions.setLookAt(
|
||||
{ x: cam[0], y: cam[1], z: cam[2] }, // position
|
||||
{ x: cam[3], y: cam[4], z: cam[5] } // target
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.embed-viewer-core {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.fullscreen {
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
position: fixed;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +0,0 @@
|
||||
import '@/bootstrapper'
|
||||
import Vue from 'vue'
|
||||
|
||||
import App from './EmbedApp.vue'
|
||||
import vuetify from './embedVuetify'
|
||||
import router from './embedRouter'
|
||||
|
||||
import '@/plugins/helpers'
|
||||
import store from '@/main/store'
|
||||
import * as MixpanelManager from '@/mixpanelManager'
|
||||
|
||||
// Init mixpanel
|
||||
MixpanelManager.initialize({
|
||||
hostApp: 'web-embed',
|
||||
hostAppDisplayName: 'Embed App'
|
||||
})
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
vuetify,
|
||||
store,
|
||||
render: (h) => h(App)
|
||||
}).$mount('#app')
|
||||
@@ -1,21 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '*',
|
||||
meta: {
|
||||
title: 'Embed View | Speckle'
|
||||
},
|
||||
component: () => import('@/embed/EmbedViewer.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,42 +0,0 @@
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import * as ThemeStateManager from '@/main/utils/themeStateManager'
|
||||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify/lib'
|
||||
|
||||
Vue.use(Vuetify)
|
||||
|
||||
ThemeStateManager.initialize()
|
||||
const isDarkMode = ThemeStateManager.isDarkTheme()
|
||||
|
||||
export default new Vuetify({
|
||||
icons: {
|
||||
iconfont: 'mdi'
|
||||
},
|
||||
theme: {
|
||||
options: { customProperties: true },
|
||||
dark: isDarkMode,
|
||||
themes: {
|
||||
light: {
|
||||
primary: '#047EFB', //blue
|
||||
secondary: '#7BBCFF', //light blue
|
||||
accent: '#FCF25E', //yellow
|
||||
error: '#FF5555', //red
|
||||
warning: '#FF9100', //orange
|
||||
info: '#313BCF', //dark blue
|
||||
success: '#4caf50',
|
||||
background: '#eeeeee',
|
||||
text: '#FFFFFF'
|
||||
},
|
||||
dark: {
|
||||
primary: '#047EFB', //blue
|
||||
secondary: '#7BBCFF', //light blue
|
||||
accent: '#FCF25E', //yellow
|
||||
error: '#FF5555', //red
|
||||
warning: '#FF9100', //orange
|
||||
info: '#313BCF', //dark blue
|
||||
success: '#4caf50',
|
||||
background: '#3a3b3c'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,70 +0,0 @@
|
||||
const SERVER_URL = window.location.origin
|
||||
|
||||
// Unauthorised fetch, without token to prevent use of localStorage or exposing elsewhere.
|
||||
async function speckleFetch(query, variables) {
|
||||
const res = await fetch(`${SERVER_URL}/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables
|
||||
})
|
||||
})
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
export const getServerInfo = () => speckleFetch(serverInfoQuery)
|
||||
|
||||
export const getStreamObj = (id) => speckleFetch(streamQuery, { id })
|
||||
|
||||
export const getBranchObj = (id, branch) => speckleFetch(branchQuery, { id, branch })
|
||||
|
||||
export const getCommitObj = (id, commit) => speckleFetch(commitQuery, { id, commit })
|
||||
|
||||
const serverInfoQuery = `
|
||||
query ServerInfo {
|
||||
serverInfo {
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const streamQuery = `
|
||||
query Stream($id: String!) {
|
||||
stream(id: $id) {
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const branchQuery = `
|
||||
query Stream($id: String!, $branch: String!) {
|
||||
stream(id: $id) {
|
||||
branch(name: $branch) {
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const commitQuery = `
|
||||
query Stream($id: String!, $commit: String!) {
|
||||
stream(id: $id) {
|
||||
commit(id: $commit) {
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -358,6 +358,27 @@ export type CommitDeleteInput = {
|
||||
streamId: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Local-only state used in CommitObjectViewer */
|
||||
export type CommitObjectViewerState = {
|
||||
__typename?: 'CommitObjectViewerState';
|
||||
addingComment: Scalars['Boolean'];
|
||||
appliedFilter?: Maybe<Scalars['JSONObject']>;
|
||||
colorLegend: Scalars['JSONObject'];
|
||||
commentReactions: Array<Scalars['String']>;
|
||||
emojis: Array<Scalars['String']>;
|
||||
hideCategoryKey?: Maybe<Scalars['String']>;
|
||||
hideCategoryValues: Array<Scalars['String']>;
|
||||
hideKey?: Maybe<Scalars['String']>;
|
||||
hideValues: Array<Scalars['String']>;
|
||||
isolateCategoryKey?: Maybe<Scalars['String']>;
|
||||
isolateCategoryValues: Array<Scalars['String']>;
|
||||
isolateKey?: Maybe<Scalars['String']>;
|
||||
isolateValues: Array<Scalars['String']>;
|
||||
preventCommentCollapse: Scalars['Boolean'];
|
||||
selectedCommentMetaData?: Maybe<SelectedCommentMetaData>;
|
||||
viewerBusy: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type CommitReceivedInput = {
|
||||
commitId: Scalars['String'];
|
||||
message?: InputMaybe<Scalars['String']>;
|
||||
@@ -814,6 +835,7 @@ export type Query = {
|
||||
* - get the comments targeting any of a set of provided resources (comments/objects): **pass in an array of resources.**
|
||||
*/
|
||||
comments?: Maybe<CommentCollection>;
|
||||
commitObjectViewerState: CommitObjectViewerState;
|
||||
serverInfo: ServerInfo;
|
||||
serverStats: ServerStats;
|
||||
/**
|
||||
@@ -958,6 +980,12 @@ export type Scope = {
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type SelectedCommentMetaData = {
|
||||
__typename?: 'SelectedCommentMetaData';
|
||||
id: Scalars['String'];
|
||||
selectionLocation: Scalars['JSONObject'];
|
||||
};
|
||||
|
||||
export type ServerApp = {
|
||||
__typename?: 'ServerApp';
|
||||
author?: Maybe<AppAuthor>;
|
||||
@@ -1688,6 +1716,21 @@ export type UpdateStreamPermissionMutationVariables = Exact<{
|
||||
|
||||
export type UpdateStreamPermissionMutation = { __typename?: 'Mutation', streamUpdatePermission?: boolean | null };
|
||||
|
||||
export type StreamFirstCommitQueryVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type StreamFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string } | null> | null } | null } | null };
|
||||
|
||||
export type StreamBranchFirstCommitQueryVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
branch: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type StreamBranchFirstCommitQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', id: string, branch?: { __typename?: 'Branch', commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, referencedObject: string } | null> | null } | null } | null } | null };
|
||||
|
||||
export type CommonUserFieldsFragment = { __typename?: 'User', id: string, suuid?: string | null, email?: string | null, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, profiles?: Record<string, unknown> | null, role?: string | null, streams?: { __typename?: 'StreamCollection', totalCount: number } | null, commits?: { __typename?: 'CommitCollectionUser', totalCount: number, items?: Array<{ __typename?: 'CommitCollectionUserNode', id: string, createdAt?: any | null } | null> | null } | null };
|
||||
|
||||
export type UserFavoriteStreamsQueryVariables = Exact<{
|
||||
@@ -2211,6 +2254,36 @@ export const UpdateStreamPermission = gql`
|
||||
streamUpdatePermission(permissionParams: $params)
|
||||
}
|
||||
`;
|
||||
export const StreamFirstCommit = gql`
|
||||
query StreamFirstCommit($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const StreamBranchFirstCommit = gql`
|
||||
query StreamBranchFirstCommit($id: String!, $branch: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
branch(name: $branch) {
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const UserFavoriteStreams = gql`
|
||||
query UserFavoriteStreams($cursor: String) {
|
||||
user {
|
||||
@@ -2418,6 +2491,8 @@ export const StreamWithCollaboratorsDocument = {"kind":"Document","definitions":
|
||||
export const StreamWithActivityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamWithActivity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"branches"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"activity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ActivityMainFields"}}]}}]}}]}}]}},...ActivityMainFieldsFragmentDoc.definitions]} as unknown as DocumentNode<StreamWithActivityQuery, StreamWithActivityQueryVariables>;
|
||||
export const LeaveStreamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"LeaveStream"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamLeave"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}]}]}}]} as unknown as DocumentNode<LeaveStreamMutation, LeaveStreamMutationVariables>;
|
||||
export const UpdateStreamPermissionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateStreamPermission"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"params"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"StreamUpdatePermissionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamUpdatePermission"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"permissionParams"},"value":{"kind":"Variable","name":{"kind":"Name","value":"params"}}}]}]}}]} as unknown as DocumentNode<UpdateStreamPermissionMutation, UpdateStreamPermissionMutationVariables>;
|
||||
export const StreamFirstCommitDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamFirstCommit"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}}]}}]}}]} as unknown as DocumentNode<StreamFirstCommitQuery, StreamFirstCommitQueryVariables>;
|
||||
export const StreamBranchFirstCommitDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"StreamBranchFirstCommit"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"branch"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"branch"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"branch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commits"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<StreamBranchFirstCommitQuery, StreamBranchFirstCommitQueryVariables>;
|
||||
export const UserFavoriteStreamsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserFavoriteStreams"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonUserFields"}},{"kind":"Field","name":{"kind":"Name","value":"favoriteStreams"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonStreamFields"}}]}}]}}]}}]}},...CommonUserFieldsFragmentDoc.definitions,...CommonStreamFieldsFragmentDoc.definitions]} as unknown as DocumentNode<UserFavoriteStreamsQuery, UserFavoriteStreamsQueryVariables>;
|
||||
export const MainUserDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MainUserData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonUserFields"}}]}}]}},...CommonUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode<MainUserDataQuery, MainUserDataQueryVariables>;
|
||||
export const ExtraUserDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ExtraUserData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonUserFields"}},{"kind":"Field","name":{"kind":"Name","value":"totalOwnedStreamsFavorites"}}]}}]}},...CommonUserFieldsFragmentDoc.definitions]} as unknown as DocumentNode<ExtraUserDataQuery, ExtraUserDataQueryVariables>;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
extend type Query {
|
||||
commitObjectViewerState: CommitObjectViewerState!
|
||||
}
|
||||
|
||||
"""
|
||||
Local-only state used in CommitObjectViewer
|
||||
"""
|
||||
type CommitObjectViewerState {
|
||||
viewerBusy: Boolean!
|
||||
appliedFilter: JSONObject
|
||||
isolateKey: String
|
||||
isolateValues: [String!]!
|
||||
hideKey: String
|
||||
hideValues: [String!]!
|
||||
colorLegend: JSONObject!
|
||||
isolateCategoryKey: String
|
||||
isolateCategoryValues: [String!]!
|
||||
hideCategoryKey: String
|
||||
hideCategoryValues: [String!]!
|
||||
selectedCommentMetaData: SelectedCommentMetaData
|
||||
addingComment: Boolean!
|
||||
preventCommentCollapse: Boolean!
|
||||
commentReactions: [String!]!
|
||||
emojis: [String!]!
|
||||
}
|
||||
|
||||
type SelectedCommentMetaData {
|
||||
id: String!
|
||||
selectionLocation: JSONObject!
|
||||
}
|
||||
@@ -118,3 +118,41 @@ export const updateStreamPermissionMutation = gql`
|
||||
streamUpdatePermission(permissionParams: $params)
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Get a stream's first commit
|
||||
*/
|
||||
export const streamFirstCommitQuery = gql`
|
||||
query StreamFirstCommit($id: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Get a stream branch's first commit
|
||||
*/
|
||||
export const streamBranchFirstCommitQuery = gql`
|
||||
query StreamBranchFirstCommit($id: String!, $branch: String!) {
|
||||
stream(id: $id) {
|
||||
id
|
||||
branch(name: $branch) {
|
||||
commits(limit: 1) {
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
referencedObject
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ReactiveVar } from '@apollo/client/core'
|
||||
import Vue, { VueConstructor } from 'vue'
|
||||
|
||||
export type Nullable<T> = T | null
|
||||
@@ -6,6 +7,13 @@ export type Optional<T> = T | undefined
|
||||
|
||||
export type MaybeFalsy<T> = T | null | undefined | false | '' | 0
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type GetReactiveVarType<V extends ReactiveVar<any>> = V extends ReactiveVar<
|
||||
infer T
|
||||
>
|
||||
? T
|
||||
: unknown
|
||||
|
||||
// Copied from Vue typings & improved ergonomics
|
||||
export type CombinedVueInstance<
|
||||
Instance extends Vue = Vue,
|
||||
|
||||
@@ -2,7 +2,6 @@ import '@/bootstrapper'
|
||||
import Vue from 'vue'
|
||||
|
||||
import App from '@/main/App.vue'
|
||||
import store from '@/main/store'
|
||||
import { LocalStorageKeys } from '@/helpers/mainConstants'
|
||||
import * as MixpanelManager from '@/mixpanelManager'
|
||||
|
||||
@@ -30,6 +29,7 @@ import PerfectScrollbar from 'vue2-perfect-scrollbar'
|
||||
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
|
||||
// adds various helper methods
|
||||
import '@/plugins/helpers'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
Vue.use(PerfectScrollbar)
|
||||
|
||||
@@ -50,8 +50,8 @@ Vue.filter('capitalize', (value) => {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1)
|
||||
})
|
||||
|
||||
const AuthToken = localStorage.getItem(LocalStorageKeys.AuthToken)
|
||||
const RefreshToken = localStorage.getItem(LocalStorageKeys.RefreshToken)
|
||||
const AuthToken = AppLocalStorage.get(LocalStorageKeys.AuthToken)
|
||||
const RefreshToken = AppLocalStorage.get(LocalStorageKeys.RefreshToken)
|
||||
|
||||
const apolloProvider = createProvider()
|
||||
installVueApollo(apolloProvider)
|
||||
@@ -92,7 +92,6 @@ function postAuthInit() {
|
||||
new Vue({
|
||||
router,
|
||||
vuetify,
|
||||
store,
|
||||
setup() {
|
||||
provide(DefaultApolloClient, apolloProvider.defaultClient)
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { signOut } from '@/plugins/authHelpers'
|
||||
import userQuery from '@/graphql/userById.gql'
|
||||
import UserAvatarIcon from '@/main/components/common/UserAvatarIcon'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
components: { UserAvatarIcon },
|
||||
@@ -26,15 +27,15 @@ export default {
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: () => localStorage.getItem('uuid')
|
||||
default: () => AppLocalStorage.get('uuid')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSelf() {
|
||||
return this.id === localStorage.getItem('uuid')
|
||||
return this.id === AppLocalStorage.get('uuid')
|
||||
},
|
||||
loggedInUserId() {
|
||||
return localStorage.getItem('uuid')
|
||||
return AppLocalStorage.get('uuid')
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
|
||||
@@ -132,6 +132,10 @@ export default {
|
||||
default: () => {
|
||||
return { role: null }
|
||||
}
|
||||
},
|
||||
streamId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
@@ -147,7 +151,7 @@ export default {
|
||||
fetchPolicy: 'no-cache',
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
id: this.comment.id
|
||||
}
|
||||
},
|
||||
@@ -169,7 +173,7 @@ export default {
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
},
|
||||
@@ -217,7 +221,7 @@ export default {
|
||||
(r) => r.resourceType !== 'stream'
|
||||
)
|
||||
const first = res.shift()
|
||||
let route = `/streams/${this.$route.params.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.commentDetails.id}`
|
||||
let route = `/streams/${this.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.commentDetails.id}`
|
||||
if (res.length !== 0) {
|
||||
route += `&overlay=${res.map((r) => r.resourceId).join(',')}`
|
||||
}
|
||||
@@ -242,7 +246,7 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
})
|
||||
@@ -256,7 +260,7 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
})
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
getBlobUrl,
|
||||
downloadBlobWithUrl
|
||||
} from '@/main/lib/common/file-upload/blobStorageApi'
|
||||
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CommentThreadAttachmentPreview',
|
||||
@@ -54,6 +55,10 @@ export default Vue.extend({
|
||||
},
|
||||
isOpen: { type: Boolean, required: true }
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId } = useCommitObjectViewerParams()
|
||||
return { streamId, resourceId }
|
||||
},
|
||||
data: () => ({
|
||||
prettyFileSize,
|
||||
blobUrl: null,
|
||||
@@ -83,7 +88,7 @@ export default Vue.extend({
|
||||
try {
|
||||
if (this.isImage) {
|
||||
this.blobUrl = await getBlobUrl(this.attachment.id, {
|
||||
streamId: this.$route.params.streamId
|
||||
streamId: this.streamId
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -83,6 +83,7 @@ import { gql } from '@apollo/client/core'
|
||||
import SmartTextEditor from '@/main/components/common/text-editor/SmartTextEditor.vue'
|
||||
import { SMART_EDITOR_SCHEMA } from '@/main/lib/viewer/comments/commentsHelper'
|
||||
import CommentThreadReplyAttachments from '@/main/components/comments/CommentThreadReplyAttachments.vue'
|
||||
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -95,6 +96,10 @@ export default {
|
||||
stream: { type: Object, default: () => null },
|
||||
index: { type: Number, default: 0 }
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId, isEmbed } = useCommitObjectViewerParams()
|
||||
return { streamId, resourceId, isEmbed }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hover: false,
|
||||
@@ -104,6 +109,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
canArchive() {
|
||||
if (this.isEmbed) return false
|
||||
if (!this.reply || !this.stream) return false
|
||||
if (this.stream.role === 'stream:owner' || this.reply.authorId === this.$userId())
|
||||
return true
|
||||
@@ -120,7 +126,7 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
commentId: this.reply.id
|
||||
}
|
||||
})
|
||||
|
||||
@@ -52,103 +52,105 @@
|
||||
@deleted="handleReplyDeleteEvent"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="$loggedIn()" class="px-0 mb-4">
|
||||
<v-slide-y-transition>
|
||||
<template v-if="!isEmbed">
|
||||
<div v-if="$loggedIn()" class="px-0 mb-4">
|
||||
<v-slide-y-transition>
|
||||
<div
|
||||
v-show="whoIsTyping.length > 0"
|
||||
class="px-4 py-2 caption mb-2 background rounded-xl"
|
||||
>
|
||||
{{ typingStatusText }}
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
<div v-if="canReply" class="d-flex mr-2">
|
||||
<comment-editor
|
||||
ref="commentEditor"
|
||||
v-model="replyValue"
|
||||
:stream-id="streamId"
|
||||
adding-comment
|
||||
max-height="300px"
|
||||
class="mb-2"
|
||||
:style="{ width: $vuetify.breakpoint.xs ? '100%' : '290px' }"
|
||||
:disabled="loadingReply"
|
||||
@input="debTypingUpdate"
|
||||
@attachments-processing="anyAttachmentsProcessing = $event"
|
||||
@submit="addReply()"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="caption background rounded-xl py-2 px-4 mr-4 elevation-2">
|
||||
You do not have sufficient permissions to reply to comments in this stream.
|
||||
</div>
|
||||
<div v-show="loadingReply" class="px-2 mb-2">
|
||||
<v-progress-linear indeterminate />
|
||||
</div>
|
||||
<div
|
||||
v-show="whoIsTyping.length > 0"
|
||||
class="px-4 py-2 caption mb-2 background rounded-xl"
|
||||
v-if="canReply"
|
||||
ref="replyinput"
|
||||
class="d-flex justify-space-between align-center comment-actions"
|
||||
>
|
||||
{{ typingStatusText }}
|
||||
<v-btn
|
||||
v-show="canArchiveThread"
|
||||
v-tooltip="'Marks this thread as archived.'"
|
||||
class="white--text ml-2"
|
||||
small
|
||||
icon
|
||||
depressed
|
||||
color="error"
|
||||
@click="showArchiveDialog = true"
|
||||
>
|
||||
<v-icon small>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
<div class="pr-5">
|
||||
<v-btn
|
||||
v-tooltip="'Copy comment url to clipboard'"
|
||||
:disabled="loadingReply"
|
||||
class="mouse elevation-5 background mr-3"
|
||||
icon
|
||||
large
|
||||
@click="copyCommentLinkToClip()"
|
||||
>
|
||||
<v-icon dark small>mdi-share-variant</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Add attachments'"
|
||||
:disabled="loadingReply"
|
||||
icon
|
||||
large
|
||||
class="mouse elevation-5 background mr-3"
|
||||
@click="addAttachments()"
|
||||
>
|
||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" small>mdi-camera</v-icon>
|
||||
<v-icon v-else small>mdi-paperclip</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Send comment (press enter)'"
|
||||
:disabled="isSubmitDisabled"
|
||||
class="mouse elevation-5 primary"
|
||||
icon
|
||||
dark
|
||||
large
|
||||
@click="addReply()"
|
||||
>
|
||||
<v-icon dark small>mdi-send</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
<div v-if="canReply" class="d-flex mr-2">
|
||||
<comment-editor
|
||||
ref="commentEditor"
|
||||
v-model="replyValue"
|
||||
:stream-id="$route.params.streamId"
|
||||
adding-comment
|
||||
max-height="300px"
|
||||
class="mb-2"
|
||||
:style="{ width: $vuetify.breakpoint.xs ? '100%' : '290px' }"
|
||||
:disabled="loadingReply"
|
||||
@input="debTypingUpdate"
|
||||
@attachments-processing="anyAttachmentsProcessing = $event"
|
||||
@submit="addReply()"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="caption background rounded-xl py-2 px-4 mr-4 elevation-2">
|
||||
You do not have sufficient permissions to reply to comments in this stream.
|
||||
</div>
|
||||
<div v-show="loadingReply" class="px-2 mb-2">
|
||||
<v-progress-linear indeterminate />
|
||||
</div>
|
||||
<div
|
||||
v-if="canReply"
|
||||
ref="replyinput"
|
||||
class="d-flex justify-space-between align-center comment-actions"
|
||||
>
|
||||
<div v-else class="pr-5">
|
||||
<v-btn
|
||||
v-show="canArchiveThread"
|
||||
v-tooltip="'Marks this thread as archived.'"
|
||||
class="white--text ml-2"
|
||||
small
|
||||
icon
|
||||
block
|
||||
depressed
|
||||
color="error"
|
||||
@click="showArchiveDialog = true"
|
||||
color="primary"
|
||||
rounded
|
||||
class="elevation-5"
|
||||
large
|
||||
@click="$loginAndSetRedirect()"
|
||||
>
|
||||
<v-icon small>mdi-delete-outline</v-icon>
|
||||
<v-icon small class="mr-1">mdi-account</v-icon>
|
||||
Sign in to reply
|
||||
</v-btn>
|
||||
<div class="pr-5">
|
||||
<v-btn
|
||||
v-tooltip="'Copy comment url to clipboard'"
|
||||
:disabled="loadingReply"
|
||||
class="mouse elevation-5 background mr-3"
|
||||
icon
|
||||
large
|
||||
@click="copyCommentLinkToClip()"
|
||||
>
|
||||
<v-icon dark small>mdi-share-variant</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Add attachments'"
|
||||
:disabled="loadingReply"
|
||||
icon
|
||||
large
|
||||
class="mouse elevation-5 background mr-3"
|
||||
@click="addAttachments()"
|
||||
>
|
||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" small>mdi-camera</v-icon>
|
||||
<v-icon v-else small>mdi-paperclip</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Send comment (press enter)'"
|
||||
:disabled="isSubmitDisabled"
|
||||
class="mouse elevation-5 primary"
|
||||
icon
|
||||
dark
|
||||
large
|
||||
@click="addReply()"
|
||||
>
|
||||
<v-icon dark small>mdi-send</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="pr-5">
|
||||
<v-btn
|
||||
block
|
||||
depressed
|
||||
color="primary"
|
||||
rounded
|
||||
class="elevation-5"
|
||||
large
|
||||
@click="$loginAndSetRedirect()"
|
||||
>
|
||||
<v-icon small class="mr-1">mdi-account</v-icon>
|
||||
Sign in to reply
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
@@ -159,8 +161,10 @@
|
||||
<div v-else-if="comment.expanded">
|
||||
<portal to="mobile-comment-thread">
|
||||
<div
|
||||
:class="`mobile-thread mouse background ${mobileExpanded ? 'expanded' : ''}`"
|
||||
style="overflow-y: scroll"
|
||||
:class="`mobile-thread mouse background ${
|
||||
mobileExpanded ? 'expanded' : ''
|
||||
} simple-scrollbar`"
|
||||
style="overflow-y: auto"
|
||||
>
|
||||
<v-card class="elevation-0" style="height: 100vh">
|
||||
<v-toolbar
|
||||
@@ -168,6 +172,7 @@
|
||||
@click.stop="mobileExpanded = !mobileExpanded"
|
||||
>
|
||||
<v-btn
|
||||
v-if="$loggedIn() && canReply && !isEmbed"
|
||||
v-tooltip="'Add attachments'"
|
||||
:disabled="loadingReply"
|
||||
icon
|
||||
@@ -244,110 +249,112 @@
|
||||
@deleted="handleReplyDeleteEvent"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="$loggedIn()" class="px-0 mb-4">
|
||||
<v-slide-y-transition>
|
||||
<template v-if="!isEmbed">
|
||||
<div v-if="$loggedIn()" class="px-0 mb-4">
|
||||
<v-slide-y-transition>
|
||||
<div
|
||||
v-show="whoIsTyping.length > 0"
|
||||
class="px-4 py-2 caption mb-2 background rounded-xl"
|
||||
>
|
||||
{{ typingStatusText }}
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
<div v-if="canReply" class="d-flex pr-5">
|
||||
<comment-editor
|
||||
ref="commentEditor"
|
||||
v-model="replyValue"
|
||||
:stream-id="streamId"
|
||||
:autofocus="false"
|
||||
adding-comment
|
||||
max-height="300px"
|
||||
class="mb-2"
|
||||
:style="{ width: $vuetify.breakpoint.xs ? '100%' : '290px' }"
|
||||
:disabled="loadingReply"
|
||||
@input="debTypingUpdate"
|
||||
@attachments-processing="anyAttachmentsProcessing = $event"
|
||||
@submit="addReply()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="whoIsTyping.length > 0"
|
||||
class="px-4 py-2 caption mb-2 background rounded-xl"
|
||||
v-else
|
||||
class="caption background rounded-xl py-2 px-4 mr-4 elevation-2"
|
||||
>
|
||||
{{ typingStatusText }}
|
||||
You do not have sufficient permissions to reply to comments in this
|
||||
stream.
|
||||
</div>
|
||||
<div v-show="loadingReply" class="px-2 mb-2">
|
||||
<v-progress-linear indeterminate />
|
||||
</div>
|
||||
<div
|
||||
v-if="canReply"
|
||||
ref="replyinput"
|
||||
class="pb-10 mb-10 d-flex justify-space-between align-center comment-actions"
|
||||
>
|
||||
<v-btn
|
||||
v-show="canArchiveThread"
|
||||
v-tooltip="'Marks this thread as archived.'"
|
||||
class="white--text ml-2"
|
||||
small
|
||||
icon
|
||||
depressed
|
||||
color="error"
|
||||
@click="showArchiveDialog = true"
|
||||
>
|
||||
<v-icon small>mdi-delete-outline</v-icon>
|
||||
</v-btn>
|
||||
<div class="pr-5">
|
||||
<v-btn
|
||||
v-tooltip="'Copy comment url to clipboard'"
|
||||
:disabled="loadingReply"
|
||||
class="mouse elevation-5 background mr-3"
|
||||
icon
|
||||
large
|
||||
@click="copyCommentLinkToClip()"
|
||||
>
|
||||
<v-icon dark small>mdi-share-variant</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Add attachments'"
|
||||
:disabled="loadingReply"
|
||||
icon
|
||||
large
|
||||
class="mouse elevation-5 background mr-3"
|
||||
@click.stop="addAttachments()"
|
||||
>
|
||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" small>
|
||||
mdi-camera-plus
|
||||
</v-icon>
|
||||
<v-icon v-else small>mdi-paperclip</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Send comment (press enter)'"
|
||||
:disabled="isSubmitDisabled"
|
||||
class="mouse elevation-5 primary"
|
||||
icon
|
||||
dark
|
||||
large
|
||||
@click="addReply()"
|
||||
>
|
||||
<v-icon dark small>mdi-send</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
<div v-if="canReply" class="d-flex pr-5">
|
||||
<comment-editor
|
||||
ref="commentEditor"
|
||||
v-model="replyValue"
|
||||
:stream-id="$route.params.streamId"
|
||||
:autofocus="false"
|
||||
adding-comment
|
||||
max-height="300px"
|
||||
class="mb-2"
|
||||
:style="{ width: $vuetify.breakpoint.xs ? '100%' : '290px' }"
|
||||
:disabled="loadingReply"
|
||||
@input="debTypingUpdate"
|
||||
@attachments-processing="anyAttachmentsProcessing = $event"
|
||||
@submit="addReply()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="caption background rounded-xl py-2 px-4 mr-4 elevation-2"
|
||||
>
|
||||
You do not have sufficient permissions to reply to comments in this
|
||||
stream.
|
||||
</div>
|
||||
<div v-show="loadingReply" class="px-2 mb-2">
|
||||
<v-progress-linear indeterminate />
|
||||
</div>
|
||||
<div
|
||||
v-if="canReply"
|
||||
ref="replyinput"
|
||||
class="pb-10 mb-10 d-flex justify-space-between align-center comment-actions"
|
||||
>
|
||||
<div v-else class="pr-5">
|
||||
<v-btn
|
||||
v-show="canArchiveThread"
|
||||
v-tooltip="'Marks this thread as archived.'"
|
||||
class="white--text ml-2"
|
||||
small
|
||||
icon
|
||||
block
|
||||
depressed
|
||||
color="error"
|
||||
@click="showArchiveDialog = true"
|
||||
color="primary"
|
||||
rounded
|
||||
class="elevation-5"
|
||||
large
|
||||
@click="$loginAndSetRedirect()"
|
||||
>
|
||||
<v-icon small>mdi-delete-outline</v-icon>
|
||||
<v-icon small class="mr-1">mdi-account</v-icon>
|
||||
Sign in to reply
|
||||
</v-btn>
|
||||
<div class="pr-5">
|
||||
<v-btn
|
||||
v-tooltip="'Copy comment url to clipboard'"
|
||||
:disabled="loadingReply"
|
||||
class="mouse elevation-5 background mr-3"
|
||||
icon
|
||||
large
|
||||
@click="copyCommentLinkToClip()"
|
||||
>
|
||||
<v-icon dark small>mdi-share-variant</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Add attachments'"
|
||||
:disabled="loadingReply"
|
||||
icon
|
||||
large
|
||||
class="mouse elevation-5 background mr-3"
|
||||
@click.stop="addAttachments()"
|
||||
>
|
||||
<v-icon v-if="$vuetify.breakpoint.smAndDown" small>
|
||||
mdi-camera-plus
|
||||
</v-icon>
|
||||
<v-icon v-else small>mdi-paperclip</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-tooltip="'Send comment (press enter)'"
|
||||
:disabled="isSubmitDisabled"
|
||||
class="mouse elevation-5 primary"
|
||||
icon
|
||||
dark
|
||||
large
|
||||
@click="addReply()"
|
||||
>
|
||||
<v-icon dark small>mdi-send</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="pr-5">
|
||||
<v-btn
|
||||
block
|
||||
depressed
|
||||
color="primary"
|
||||
rounded
|
||||
class="elevation-5"
|
||||
large
|
||||
@click="$loginAndSetRedirect()"
|
||||
>
|
||||
<v-icon small class="mr-1">mdi-account</v-icon>
|
||||
Sign in to reply
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
@@ -385,6 +392,9 @@ import { isDocEmpty } from '@/main/lib/common/text-editor/documentHelper'
|
||||
import { SMART_EDITOR_SCHEMA } from '@/main/lib/viewer/comments/commentsHelper'
|
||||
import { isSuccessfullyUploaded } from '@/main/lib/common/file-upload/fileUploadHelper'
|
||||
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
|
||||
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
// TODO: The template is a WET mess, need to refactor it
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -419,7 +429,7 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { streamId: this.$route.params.streamId }
|
||||
return { streamId: this.streamId }
|
||||
}
|
||||
},
|
||||
replyQuery: {
|
||||
@@ -443,7 +453,7 @@ export default {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
id: this.comment.id
|
||||
}
|
||||
},
|
||||
@@ -472,7 +482,7 @@ export default {
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
},
|
||||
@@ -489,7 +499,7 @@ export default {
|
||||
}, 100)
|
||||
}
|
||||
this.localReplies.push(data.commentThreadActivity.reply)
|
||||
this.$refs.replyinput.scrollIntoView({ behaviour: 'smooth', block: 'end' })
|
||||
this.$refs.replyinput?.scrollIntoView({ behaviour: 'smooth', block: 'end' })
|
||||
return
|
||||
}
|
||||
if (data.commentThreadActivity.type === 'comment-archived') {
|
||||
@@ -517,6 +527,14 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId, isEmbed } = useCommitObjectViewerParams()
|
||||
return {
|
||||
streamId,
|
||||
resourceId,
|
||||
isEmbed
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hovered: true,
|
||||
@@ -557,7 +575,7 @@ export default {
|
||||
return [this.comment, ...sorted]
|
||||
},
|
||||
isComplete() {
|
||||
const res = [this.$route.params.resourceId]
|
||||
const res = [this.resourceId]
|
||||
if (this.$route.query.overlay) res.push(...this.$route.query.overlay.split(','))
|
||||
const commRes = this.comment.resources
|
||||
.filter((r) => r.resourceType !== 'stream')
|
||||
@@ -572,7 +590,7 @@ export default {
|
||||
if (!this.comment) return
|
||||
const res = this.comment.resources.filter((r) => r.resourceType !== 'stream')
|
||||
const first = res.shift()
|
||||
let route = `/streams/${this.$route.params.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.comment.id}`
|
||||
let route = `/streams/${this.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.comment.id}`
|
||||
if (res.length !== 0) {
|
||||
route += `&overlay=${res.map((r) => r.resourceId).join(',')}`
|
||||
}
|
||||
@@ -600,7 +618,7 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
})
|
||||
@@ -667,7 +685,7 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
sId: this.$route.params.streamId,
|
||||
sId: this.streamId,
|
||||
cId: this.comment.id,
|
||||
d: {
|
||||
userId: this.$userId(),
|
||||
@@ -680,7 +698,7 @@ export default {
|
||||
copyCommentLinkToClip() {
|
||||
const res = this.comment.resources.filter((r) => r.resourceType !== 'stream')
|
||||
const first = res.shift()
|
||||
let route = `${window.origin}/streams/${this.$route.params.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.comment.id}`
|
||||
let route = `${window.origin}/streams/${this.streamId}/${first.resourceType}s/${first.resourceId}?cId=${this.comment.id}`
|
||||
if (res.length !== 0) {
|
||||
route += `&overlay=${res.map((r) => r.resourceId).join(',')}`
|
||||
}
|
||||
@@ -691,7 +709,7 @@ export default {
|
||||
})
|
||||
},
|
||||
addMissingResources() {
|
||||
const res = [this.$route.params.resourceId]
|
||||
const res = [this.resourceId]
|
||||
if (this.$route.query.overlay) res.push(...this.$route.query.overlay.split(','))
|
||||
const commRes = this.comment.resources
|
||||
.filter((r) => r.resourceType !== 'stream')
|
||||
@@ -725,7 +743,7 @@ export default {
|
||||
.filter(isSuccessfullyUploaded)
|
||||
.map((a) => a.result.blobId)
|
||||
const replyInput = {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
parentComment: this.comment.id,
|
||||
text: this.replyValue.doc,
|
||||
blobIds
|
||||
@@ -787,7 +805,7 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
commentId: this.comment.id
|
||||
}
|
||||
})
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
:key="comment.id + '-card-sidebar'"
|
||||
no-gutters
|
||||
:class="`${isUnread(comment) ? 'border' : ''} my-2 property-row rounded-lg ${
|
||||
$store.state.selectedComment &&
|
||||
$store.state.selectedComment.id === comment.id
|
||||
viewerState.selectedCommentMetaData &&
|
||||
viewerState.selectedCommentMetaData.id === comment.id
|
||||
? 'elevation-5 selected'
|
||||
: ''
|
||||
}`"
|
||||
@@ -109,10 +109,11 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn
|
||||
v-if="!isEmbed"
|
||||
small
|
||||
block
|
||||
class="rounded-xl"
|
||||
:to="`/streams/${$route.params.streamId}/comments`"
|
||||
:to="`/streams/${streamId}/comments`"
|
||||
>
|
||||
all stream comments
|
||||
</v-btn>
|
||||
@@ -122,6 +123,10 @@
|
||||
</template>
|
||||
<script>
|
||||
import { documentToBasicString } from '@/main/lib/common/text-editor/documentHelper'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import gql from 'graphql-tag'
|
||||
import { computed } from 'vue'
|
||||
import { useCommitObjectViewerParams } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -137,6 +142,21 @@ export default {
|
||||
default: 'all'
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId, isEmbed } = useCommitObjectViewerParams()
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
selectedCommentMetaData
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewerState, streamId, resourceId, isEmbed }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expand: true,
|
||||
|
||||
@@ -94,7 +94,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
streamId() {
|
||||
return this.commit.streamId ?? this.$route.params.streamId
|
||||
return (
|
||||
this.commit.streamId ?? this.$route.params.streamId ?? this.$route.query.stream
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
url: {
|
||||
@@ -168,11 +170,10 @@ export default {
|
||||
this.imageIndex = index
|
||||
},
|
||||
async getPreviewImage(angle = 0) {
|
||||
const authToken = AppLocalStorage.get('AuthToken')
|
||||
const res = await fetch(this.url + `/${angle}`, {
|
||||
signal: this.controller.signal,
|
||||
headers: localStorage.getItem('AuthToken')
|
||||
? { Authorization: `Bearer ${localStorage.getItem('AuthToken')}` }
|
||||
: {}
|
||||
headers: authToken ? { Authorization: `Bearer ${authToken}` } : {}
|
||||
})
|
||||
|
||||
if (res.headers.has('X-Preview-Error')) {
|
||||
|
||||
@@ -14,20 +14,17 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { Viewer, DefaultViewerParams } from '@speckle/viewer'
|
||||
import throttle from 'lodash/throttle'
|
||||
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
|
||||
|
||||
export default {
|
||||
name: 'SpeckleViewer',
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
watch: {
|
||||
fullScreen() {
|
||||
setTimeout(() => {
|
||||
window.__viewer.onWindowResize()
|
||||
window.__viewer.cameraHandler.onWindowResize()
|
||||
}, 20)
|
||||
setup() {
|
||||
const { viewer, container, isInitializedPromise } = useInjectedViewer()
|
||||
return {
|
||||
viewer,
|
||||
viewerContainer: container,
|
||||
isViewerInitializedPromise: isInitializedPromise
|
||||
}
|
||||
},
|
||||
// TODO: pause rendering on destroy, reinit on mounted.
|
||||
@@ -41,29 +38,26 @@ export default {
|
||||
// - juggle the container div back in of this component's dom when it's back.
|
||||
|
||||
this.$mixpanel.track('Viewer Action', { type: 'action', name: 'load' })
|
||||
let renderDomElement = document.getElementById('renderer')
|
||||
if (!renderDomElement) {
|
||||
renderDomElement = document.createElement('div')
|
||||
renderDomElement.id = 'renderer'
|
||||
}
|
||||
if (!window.__viewer) {
|
||||
window.__viewer = new Viewer(renderDomElement, DefaultViewerParams)
|
||||
await window.__viewer.init()
|
||||
|
||||
if (!this.viewer || !this.viewerContainer || !this.isViewerInitializedPromise) {
|
||||
throw new Error('Viewer or its container not properly injected!')
|
||||
}
|
||||
|
||||
this.domElement = renderDomElement
|
||||
await this.isViewerInitializedPromise
|
||||
|
||||
this.domElement = this.viewerContainer
|
||||
this.domElement.style.display = 'inline-block'
|
||||
this.$refs.rendererparent.appendChild(renderDomElement)
|
||||
this.$refs.rendererparent.appendChild(this.domElement)
|
||||
|
||||
await window.__viewer.unloadAll()
|
||||
await this.viewer.unloadAll()
|
||||
|
||||
window.__viewer.onWindowResize()
|
||||
window.__viewer.cameraHandler.onWindowResize()
|
||||
this.viewer.onWindowResize()
|
||||
this.viewer.cameraHandler.onWindowResize()
|
||||
this.setupEvents()
|
||||
this.$emit('viewer-init')
|
||||
this.$eventHub.$on('resize-viewer', () => {
|
||||
window.__viewer.onWindowResize()
|
||||
window.__viewer.cameraHandler.onWindowResize()
|
||||
this.viewer.onWindowResize()
|
||||
this.viewer.cameraHandler.onWindowResize()
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
@@ -73,21 +67,21 @@ export default {
|
||||
this.domElement.style.display = 'none'
|
||||
// move renderer dom element outside this component so it doesn't get deleted.
|
||||
document.body.appendChild(this.domElement)
|
||||
window.__viewer.unloadAll()
|
||||
this.viewer.unloadAll()
|
||||
},
|
||||
methods: {
|
||||
setupEvents() {
|
||||
window.__viewer.on('load-warning', ({ message }) => {
|
||||
this.viewer.on('load-warning', ({ message }) => {
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: message
|
||||
})
|
||||
})
|
||||
|
||||
window.__viewer.on(
|
||||
this.viewer.on(
|
||||
'load-progress',
|
||||
throttle((args) => this.$emit('load-progress', args), 250)
|
||||
)
|
||||
window.__viewer.on('select', (objects) => this.$emit('selection', objects))
|
||||
this.viewer.on('select', (objects) => this.$emit('selection', objects))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div style="display: inline-block">
|
||||
<v-menu v-if="$loggedIn()" offset-x open-on-hover :close-on-content-click="false">
|
||||
<v-menu v-if="isLoggedIn" offset-x open-on-hover :close-on-content-click="false">
|
||||
<template #activator="{ on, attrs }">
|
||||
<div v-bind="attrs" v-on="on">
|
||||
<user-avatar-icon
|
||||
@@ -72,6 +72,9 @@
|
||||
<script>
|
||||
import userByIdQuery from '@/graphql/userById.gql'
|
||||
import UserAvatarIcon from '@/main/components/common/UserAvatarIcon'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
import { LocalStorageKeys } from '@/helpers/mainConstants'
|
||||
import { useIsLoggedIn } from '@/main/lib/core/composables/auth'
|
||||
|
||||
export default {
|
||||
components: { UserAvatarIcon },
|
||||
@@ -99,9 +102,13 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { isLoggedIn } = useIsLoggedIn()
|
||||
return { isLoggedIn }
|
||||
},
|
||||
computed: {
|
||||
isSelf() {
|
||||
return this.id === localStorage.getItem('uuid')
|
||||
return this.id === AppLocalStorage.get(LocalStorageKeys.Uuid)
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
@@ -113,7 +120,7 @@ export default {
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.$loggedIn
|
||||
return !this.isLoggedIn
|
||||
},
|
||||
update: (data) => {
|
||||
return data.user
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<portal v-if="canRender" :to="to">
|
||||
<slot />
|
||||
</portal>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { usePortalState } from '@/main/utils/portalStateManager'
|
||||
import { computed, defineComponent } from 'vue'
|
||||
|
||||
/**
|
||||
* This component should be used instead of <portal> when you have multiple portals that try to use the same <portal-target>,
|
||||
* possibly at the same time. The priority key will help choose which portal will actually render in the target.
|
||||
*/
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PrioritizedPortal',
|
||||
props: {
|
||||
/**
|
||||
* Name of portal-target that this portal should reach out to
|
||||
*/
|
||||
to: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* Unique identity of this specific portal entrypoint
|
||||
*/
|
||||
identity: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* Priority helps figure out which portal entrypoint should take precedence if multiple portals
|
||||
* attempt to use the same portal-target. A higher number = higher priority.
|
||||
*/
|
||||
priority: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { allowedPortals } = usePortalState(
|
||||
[props.to],
|
||||
props.identity,
|
||||
props.priority
|
||||
)
|
||||
const canRender = computed(() => allowedPortals.value[props.to])
|
||||
|
||||
return {
|
||||
canRender
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -81,6 +81,7 @@ import ListItemActivity from '@/main/components/activity/ListItemActivity.vue'
|
||||
import { UserTimelineDocument } from '@/graphql/generated/graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
name: 'FeedTimeline',
|
||||
@@ -196,7 +197,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
timeline(val) {
|
||||
if (val.totalCount === 0 && !localStorage.getItem('onboarding')) {
|
||||
if (val.totalCount === 0 && !AppLocalStorage.get('onboarding')) {
|
||||
this.$router.push('/onboarding')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,6 +294,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
length: 5,
|
||||
@@ -356,11 +358,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
skip() {
|
||||
localStorage.setItem('onboarding', 'skipped')
|
||||
AppLocalStorage.set('onboarding', 'skipped')
|
||||
this.$router.push('/')
|
||||
},
|
||||
finish() {
|
||||
localStorage.setItem('onboarding', 'complete')
|
||||
AppLocalStorage.set('onboarding', 'complete')
|
||||
this.$router.push('/')
|
||||
},
|
||||
prev() {
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -123,7 +124,7 @@ export default {
|
||||
`/api/stream/${this.$route.params.streamId}/blob/${this.fileId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: localStorage.getItem('AuthToken')
|
||||
Authorization: AppLocalStorage.get('AuthToken')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
file: {
|
||||
@@ -71,7 +73,7 @@ export default {
|
||||
)
|
||||
request.setRequestHeader(
|
||||
'Authorization',
|
||||
`Bearer ${localStorage.getItem('AuthToken')}`
|
||||
`Bearer ${AppLocalStorage.get('AuthToken')}`
|
||||
)
|
||||
|
||||
request.upload.addEventListener(
|
||||
|
||||
@@ -43,8 +43,9 @@
|
||||
</v-alert>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
user: {
|
||||
@@ -67,7 +68,7 @@ export default {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: localStorage.getItem('AuthToken')
|
||||
Authorization: AppLocalStorage.get('AuthToken')
|
||||
},
|
||||
body: JSON.stringify({ email: this.user.email })
|
||||
})
|
||||
|
||||
@@ -111,6 +111,8 @@
|
||||
</template>
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VImageInput: () => import('vuetify-image-input/a-la-carte'),
|
||||
@@ -133,7 +135,7 @@ export default {
|
||||
computed: {
|
||||
isSelf() {
|
||||
if (!this.user) return false
|
||||
return this.user.id === localStorage.getItem('uuid')
|
||||
return this.user.id === AppLocalStorage.get('uuid')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -28,11 +28,18 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
<script lang="ts">
|
||||
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
small: { type: Boolean, default: false }
|
||||
},
|
||||
setup() {
|
||||
const { viewer } = useInjectedViewer()
|
||||
return { viewer }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [
|
||||
@@ -45,11 +52,11 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setView(view) {
|
||||
window.__viewer.interactions.rotateTo(view.toLowerCase())
|
||||
setView(view: string) {
|
||||
this.viewer.interactions.rotateTo(view.toLowerCase())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.test {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
>
|
||||
<v-slide-x-transition>
|
||||
<div
|
||||
v-show="visible && !$store.state.selectedComment"
|
||||
v-show="visible && !viewerState.selectedCommentMetaData"
|
||||
ref="commentButton"
|
||||
class="new-comment-overlay absolute-pos"
|
||||
>
|
||||
@@ -44,7 +44,7 @@
|
||||
<comment-editor
|
||||
ref="desktopEditor"
|
||||
v-model="commentValue"
|
||||
:stream-id="$route.params.streamId"
|
||||
:stream-id="streamId"
|
||||
adding-comment
|
||||
style="width: 300px"
|
||||
max-height="300px"
|
||||
@@ -59,7 +59,7 @@
|
||||
>
|
||||
<v-fade-transition group>
|
||||
<template v-if="isCommentEmpty">
|
||||
<template v-for="reaction in $store.state.commentReactions">
|
||||
<template v-for="reaction in viewerState.commentReactions">
|
||||
<v-btn
|
||||
:key="reaction"
|
||||
class="mr-2"
|
||||
@@ -142,7 +142,7 @@
|
||||
<comment-editor
|
||||
ref="mobileEditor"
|
||||
v-model="commentValue"
|
||||
:stream-id="$route.params.streamId"
|
||||
:stream-id="streamId"
|
||||
adding-comment
|
||||
style="width: 100%"
|
||||
max-height="60vh"
|
||||
@@ -169,7 +169,7 @@
|
||||
</v-btn>
|
||||
<v-fade-transition group>
|
||||
<template v-if="isCommentEmpty">
|
||||
<template v-for="reaction in $store.state.commentReactions">
|
||||
<template v-for="reaction in viewerState.commentReactions">
|
||||
<v-btn
|
||||
:key="reaction"
|
||||
class="mr-2 elevation-4"
|
||||
@@ -214,7 +214,7 @@
|
||||
<portal to="viewercontrols" :order="100">
|
||||
<v-slide-x-transition>
|
||||
<v-btn
|
||||
v-show="!location && !$store.state.selectedComment"
|
||||
v-show="!location && !viewerState.selectedCommentMetaData"
|
||||
v-tooltip="'Add a comment (ctrl + shift + c)'"
|
||||
icon
|
||||
dark
|
||||
@@ -233,7 +233,6 @@
|
||||
import * as THREE from 'three'
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { debounce, throttle } from 'lodash'
|
||||
import { getCamArray } from './viewerFrontendHelpers'
|
||||
import CommentEditor from '@/main/components/comments/CommentEditor.vue'
|
||||
import {
|
||||
basicStringToDocument,
|
||||
@@ -245,6 +244,14 @@ import {
|
||||
} from '@/main/lib/viewer/comments/commentsHelper'
|
||||
import { buildResizeHandlerMixin } from '@/main/lib/common/web-apis/mixins/windowResizeHandler'
|
||||
import { isSuccessfullyUploaded } from '@/main/lib/common/file-upload/fileUploadHelper'
|
||||
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
|
||||
import { getCamArray } from '@/main/lib/viewer/core/helpers/cameraHelper'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
setIsAddingComment,
|
||||
useCommitObjectViewerParams
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
/**
|
||||
* TODO: Would be nice to get rid of duplicate templates for mobile & large screens
|
||||
@@ -280,10 +287,28 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return { streamId: this.$route.params.streamId }
|
||||
return { streamId: this.streamId }
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId } = useCommitObjectViewerParams()
|
||||
const { viewer } = useInjectedViewer()
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
selectedCommentMetaData
|
||||
commentReactions
|
||||
appliedFilter
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewer, viewerState, streamId, resourceId }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
location: null,
|
||||
@@ -308,14 +333,14 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.viewerSelectHandler = debounce(this.handleSelect, 10)
|
||||
window.__viewer.on('select', this.viewerSelectHandler)
|
||||
this.viewer.on('select', this.viewerSelectHandler)
|
||||
|
||||
// Throttling update, cause it happens way too often and triggers expensive DOM updates
|
||||
// Smoothing out the animation with CSS transitions (check style)
|
||||
this.viewerControlsUpdateHandler = throttle(() => {
|
||||
this.updateCommentBubble()
|
||||
}, VIEWER_UPDATE_THROTTLE_TIME)
|
||||
window.__viewer.cameraHandler.controls.addEventListener(
|
||||
this.viewer.cameraHandler.controls.addEventListener(
|
||||
'update',
|
||||
this.viewerControlsUpdateHandler
|
||||
)
|
||||
@@ -326,8 +351,8 @@ export default {
|
||||
document.addEventListener('keyup', this.docKeyUpHandler)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.__viewer.removeListener('select', this.viewerSelectHandler)
|
||||
window.__viewer.cameraHandler.controls.removeEventListener(
|
||||
this.viewer.removeListener('select', this.viewerSelectHandler)
|
||||
this.viewer.cameraHandler.controls.removeEventListener(
|
||||
'update',
|
||||
this.viewerControlsUpdateHandler
|
||||
)
|
||||
@@ -356,17 +381,17 @@ export default {
|
||||
|
||||
this.$mixpanel.track('Comment Action', { type: 'action', name: 'create' })
|
||||
|
||||
const camTarget = window.__viewer.cameraHandler.activeCam.controls.getTarget()
|
||||
const camTarget = this.viewer.cameraHandler.activeCam.controls.getTarget()
|
||||
|
||||
const blobIds = this.commentValue.attachments
|
||||
.filter(isSuccessfullyUploaded)
|
||||
.map((a) => a.result.blobId)
|
||||
const commentInput = {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
resources: [
|
||||
{
|
||||
resourceType: this.$route.path.includes('object') ? 'object' : 'commit',
|
||||
resourceId: this.$route.params.resourceId
|
||||
resourceId: this.resourceId
|
||||
}
|
||||
],
|
||||
text: this.commentValue.doc,
|
||||
@@ -375,12 +400,12 @@ export default {
|
||||
location: this.location
|
||||
? this.location
|
||||
: new THREE.Vector3(camTarget.x, camTarget.y, camTarget.z),
|
||||
camPos: getCamArray(),
|
||||
filters: this.$store.state.appliedFilter,
|
||||
sectionBox: window.__viewer.sectionBox.getCurrentBox(),
|
||||
camPos: getCamArray(this.viewer),
|
||||
filters: this.viewerState.appliedFilter,
|
||||
sectionBox: this.viewer.sectionBox.getCurrentBox(),
|
||||
selection: null // TODO for later, lazy now
|
||||
},
|
||||
screenshot: window.__viewer.interactions.screenshot()
|
||||
screenshot: this.viewer.interactions.screenshot()
|
||||
}
|
||||
if (this.$route.query.overlay) {
|
||||
commentInput.resources.push(
|
||||
@@ -419,8 +444,8 @@ export default {
|
||||
this.expand = false
|
||||
this.visible = false
|
||||
this.commentValue = { doc: null, attachments: [] }
|
||||
this.$store.commit('setAddingCommentState', { addingCommentState: false })
|
||||
window.__viewer.interactions.deselectObjects()
|
||||
setIsAddingComment(false)
|
||||
this.viewer.interactions.deselectObjects()
|
||||
},
|
||||
sendStatusUpdate() {
|
||||
// TODO: typing or not
|
||||
@@ -436,7 +461,7 @@ export default {
|
||||
|
||||
if (!this.location && !this.expand) this.visible = false
|
||||
|
||||
this.$store.commit('setAddingCommentState', { addingCommentState: this.expand })
|
||||
setIsAddingComment(this.expand)
|
||||
},
|
||||
handleSelect(info) {
|
||||
this.expand = false
|
||||
@@ -444,7 +469,7 @@ export default {
|
||||
// TODO: deselect event
|
||||
this.visible = false
|
||||
this.location = null
|
||||
this.$store.commit('setAddingCommentState', { addingCommentState: false })
|
||||
setIsAddingComment(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -462,7 +487,7 @@ export default {
|
||||
info.location.z
|
||||
)
|
||||
|
||||
const cam = window.__viewer.cameraHandler.camera
|
||||
const cam = this.viewer.cameraHandler.camera
|
||||
cam.updateProjectionMatrix()
|
||||
projectedLocation.project(cam)
|
||||
let collapsedSize = this.$refs.commentButton.clientWidth
|
||||
@@ -483,7 +508,7 @@ export default {
|
||||
// TODO: Clamping, etc.
|
||||
if (!this.location) return
|
||||
if (!this.$refs.commentButton) return
|
||||
const cam = window.__viewer.cameraHandler.camera
|
||||
const cam = this.viewer.cameraHandler.camera
|
||||
cam.updateProjectionMatrix()
|
||||
const projectedLocation = this.location.clone()
|
||||
projectedLocation.project(cam)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
class="d-flex align-center justify-center no-mouse"
|
||||
>
|
||||
<div
|
||||
v-show="showComments && !$store.state.addingComment"
|
||||
v-show="showComments && !viewerState.addingComment"
|
||||
style="
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
@@ -175,6 +175,16 @@ import { VIEWER_UPDATE_THROTTLE_TIME } from '@/main/lib/viewer/comments/comments
|
||||
import { buildResizeHandlerMixin } from '@/main/lib/common/web-apis/mixins/windowResizeHandler'
|
||||
import { documentToBasicString } from '@/main/lib/common/text-editor/documentHelper'
|
||||
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
|
||||
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
resetFilter,
|
||||
setFilterDirectly,
|
||||
setPreventCommentCollapse,
|
||||
setSelectedCommentMetaData,
|
||||
useCommitObjectViewerParams
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -204,8 +214,8 @@ export default {
|
||||
variables() {
|
||||
const resourceArr = [
|
||||
{
|
||||
resourceType: this.$resourceType(this.$route.params.resourceId),
|
||||
resourceId: this.$route.params.resourceId
|
||||
resourceType: this.$resourceType(this.resourceId),
|
||||
resourceId: this.resourceId
|
||||
}
|
||||
]
|
||||
if (this.$route.query.overlay) {
|
||||
@@ -218,7 +228,7 @@ export default {
|
||||
}
|
||||
|
||||
return {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
resources: resourceArr
|
||||
}
|
||||
},
|
||||
@@ -255,11 +265,11 @@ export default {
|
||||
${COMMENT_FULL_INFO_FRAGMENT}
|
||||
`,
|
||||
variables() {
|
||||
let resIds = [this.$route.params.resourceId]
|
||||
let resIds = [this.resourceId]
|
||||
if (this.$route.query.overlay)
|
||||
resIds = [...resIds, ...this.$route.query.overlay.split(',')]
|
||||
return {
|
||||
streamId: this.$route.params.streamId,
|
||||
streamId: this.streamId,
|
||||
resourceIds: resIds
|
||||
}
|
||||
},
|
||||
@@ -297,6 +307,25 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId } = useCommitObjectViewerParams()
|
||||
const { viewer } = useInjectedViewer()
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
addingComment
|
||||
viewerBusy
|
||||
preventCommentCollapse
|
||||
emojis
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewer, viewerState, streamId, resourceId }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localComments: [],
|
||||
@@ -330,7 +359,7 @@ export default {
|
||||
if (this.$route.query.cId) {
|
||||
this.openCommentOnInit = this.$route.query.cId
|
||||
this.commentIntervalChecker = window.setInterval(() => {
|
||||
if (this.$store.state.viewerBusy || this.$apollo.loading) return
|
||||
if (this.viewerState.viewerBusy || this.$apollo.loading) return
|
||||
this.expandComment({ id: this.openCommentOnInit })
|
||||
this.openCommentOnInit = null
|
||||
const q = { ...this.$route.query }
|
||||
@@ -345,15 +374,15 @@ export default {
|
||||
|
||||
this.viewerSelectHandler = debounce(() => {
|
||||
// prevents comment collapse if filters are reset (that triggers a deselect event from the viewer)
|
||||
if (this.$store.state.preventCommentCollapse) {
|
||||
this.$store.commit('setPreventCommentCollapse', { value: false })
|
||||
if (this.viewerState.preventCommentCollapse) {
|
||||
setPreventCommentCollapse(false)
|
||||
return
|
||||
}
|
||||
for (const c of this.localComments) {
|
||||
this.collapseComment(c)
|
||||
}
|
||||
}, 10)
|
||||
window.__viewer.on('select', this.viewerSelectHandler)
|
||||
this.viewer.on('select', this.viewerSelectHandler)
|
||||
|
||||
// Throttling update, cause it happens way too often and triggers expensive DOM updates
|
||||
// Smoothing out the animation with CSS transitions (check style)
|
||||
@@ -361,7 +390,7 @@ export default {
|
||||
// console.log('cameraHandler.controls update')
|
||||
this.updateCommentBubbles()
|
||||
}, VIEWER_UPDATE_THROTTLE_TIME)
|
||||
window.__viewer.cameraHandler.controls.addEventListener(
|
||||
this.viewer.cameraHandler.controls.addEventListener(
|
||||
'update',
|
||||
this.viewerControlsUpdateHandler
|
||||
)
|
||||
@@ -371,8 +400,8 @@ export default {
|
||||
}, 1000)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.__viewer.removeListener('select', this.viewerSelectHandler)
|
||||
window.__viewer.cameraHandler.controls.removeEventListener(
|
||||
this.viewer.removeListener('select', this.viewerSelectHandler)
|
||||
this.viewer.cameraHandler.controls.removeEventListener(
|
||||
'update',
|
||||
this.viewerControlsUpdateHandler
|
||||
)
|
||||
@@ -384,7 +413,7 @@ export default {
|
||||
this.updateCommentBubbles()
|
||||
},
|
||||
getLeadingEmoji(comment) {
|
||||
const emojiWhitelist = this.$store.state.emojis
|
||||
const emojiWhitelist = this.viewerState.emojis
|
||||
const commentPureText = documentToBasicString(comment.text.doc, 1)
|
||||
const emojiCandidate = commentPureText.split(' ')[0]
|
||||
return emojiWhitelist.includes(emojiCandidate) ? emojiCandidate : null
|
||||
@@ -426,7 +455,7 @@ export default {
|
||||
for (const c of this.localComments) {
|
||||
if (c.id === comment.id) {
|
||||
c.preventAutoClose = true
|
||||
this.$store.commit('setCommentSelection', { comment: c })
|
||||
setSelectedCommentMetaData(c)
|
||||
this.setCommentPow(c)
|
||||
setTimeout(() => {
|
||||
c.expanded = true
|
||||
@@ -448,35 +477,38 @@ export default {
|
||||
for (const c of this.localComments) {
|
||||
if (c.id === comment.id && c.expanded) {
|
||||
c.expanded = false
|
||||
if (c.data.filters) this.$store.commit('resetFilter')
|
||||
if (c.data.sectionBox) window.__viewer.sectionBox.off()
|
||||
this.$store.commit('setCommentSelection', { comment: null })
|
||||
if (c.data.filters) resetFilter()
|
||||
if (c.data.sectionBox) this.viewer.sectionBox.off()
|
||||
|
||||
setSelectedCommentMetaData(null)
|
||||
}
|
||||
}
|
||||
},
|
||||
setCommentPow(comment) {
|
||||
const camToSet = comment.data.camPos
|
||||
if (camToSet[6] === 1) {
|
||||
window.__viewer.toggleCameraProjection()
|
||||
this.viewer.toggleCameraProjection()
|
||||
}
|
||||
window.__viewer.interactions.setLookAt(
|
||||
this.viewer.interactions.setLookAt(
|
||||
{ x: camToSet[0], y: camToSet[1], z: camToSet[2] }, // position
|
||||
{ x: camToSet[3], y: camToSet[4], z: camToSet[5] } // target
|
||||
)
|
||||
if (camToSet[6] === 1) {
|
||||
window.__viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
|
||||
this.viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
|
||||
}
|
||||
if (comment.data.filters) {
|
||||
this.$store.commit('setFilterDirect', { filter: comment.data.filters })
|
||||
setFilterDirectly({
|
||||
filter: comment.data.filters
|
||||
})
|
||||
} else {
|
||||
this.$store.commit('resetFilter')
|
||||
resetFilter()
|
||||
}
|
||||
|
||||
if (comment.data.sectionBox) {
|
||||
window.__viewer.sectionBox.setBox(comment.data.sectionBox, 0)
|
||||
window.__viewer.sectionBox.on()
|
||||
this.viewer.sectionBox.setBox(comment.data.sectionBox, 0)
|
||||
this.viewer.sectionBox.on()
|
||||
} else {
|
||||
window.__viewer.sectionBox.off()
|
||||
this.viewer.sectionBox.off()
|
||||
}
|
||||
},
|
||||
async handleDeletion(comment) {
|
||||
@@ -488,7 +520,7 @@ export default {
|
||||
updateCommentBubbles() {
|
||||
// console.log('updateCommentBubbles', new Date().toISOString())
|
||||
if (!this.comments) return
|
||||
const cam = window.__viewer.cameraHandler.camera
|
||||
const cam = this.viewer.cameraHandler.camera
|
||||
cam.updateProjectionMatrix()
|
||||
for (const comment of this.localComments) {
|
||||
// get html elements
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div @mouseenter="hovered = true" @mouseleave="hovered = false">
|
||||
<v-card
|
||||
class="mx-2 my-4 rounded-lg"
|
||||
class="mx-2 rounded-lg"
|
||||
:elevation="`${hovered ? 10 : 2}`"
|
||||
style="transition: all 0.2s ease"
|
||||
>
|
||||
@@ -22,7 +22,7 @@
|
||||
<source-app-avatar :application-name="commit.sourceApplication" />
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="$route.params.resourceId !== resource.id"
|
||||
v-if="resourceId !== resource.id"
|
||||
v-tooltip="'Remove'"
|
||||
small
|
||||
icon
|
||||
@@ -77,6 +77,16 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import gql from 'graphql-tag'
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
hideObjects,
|
||||
isolateObjects,
|
||||
showObjects,
|
||||
unisolateObjects,
|
||||
useCommitObjectViewerParams
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
export default {
|
||||
components: {
|
||||
SourceAppAvatar: () => import('@/main/components/common/SourceAppAvatar'),
|
||||
@@ -89,6 +99,22 @@ export default {
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId } = useCommitObjectViewerParams()
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
isolateValues
|
||||
hideValues
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewerState, streamId, resourceId }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
@@ -101,14 +127,14 @@ export default {
|
||||
},
|
||||
isolated() {
|
||||
return (
|
||||
this.$store.state.isolateValues.indexOf(
|
||||
this.viewerState.isolateValues.indexOf(
|
||||
this.resource.data.commit.referencedObject
|
||||
) !== -1
|
||||
)
|
||||
},
|
||||
visible() {
|
||||
return (
|
||||
this.$store.state.hideValues.indexOf(
|
||||
this.viewerState.hideValues.indexOf(
|
||||
this.resource.data.commit.referencedObject
|
||||
) === -1
|
||||
)
|
||||
@@ -118,12 +144,12 @@ export default {
|
||||
isolate() {
|
||||
const id = this.resource.data.commit.referencedObject
|
||||
if (this.isolated)
|
||||
this.$store.commit('unisolateObjects', {
|
||||
unisolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: [id]
|
||||
})
|
||||
else
|
||||
this.$store.commit('isolateObjects', {
|
||||
isolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: [id]
|
||||
})
|
||||
@@ -131,12 +157,12 @@ export default {
|
||||
toggleVisibility() {
|
||||
const id = this.resource.data.commit.referencedObject
|
||||
if (this.visible)
|
||||
this.$store.commit('hideObjects', {
|
||||
hideObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: [id]
|
||||
})
|
||||
else
|
||||
this.$store.commit('showObjects', {
|
||||
showObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: [id]
|
||||
})
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { setupCommitObjectViewer } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
import { defineComponent, toRefs } from 'vue'
|
||||
|
||||
/**
|
||||
* Component's only use is to extend injection scope in portals, where it gets lost
|
||||
* https://portal-vue.linusb.org/guide/caveats.html#local-state-lost-when-toggling-disabled
|
||||
*/
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CommitObjectViewerScope',
|
||||
props: {
|
||||
streamId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
resourceId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isEmbed: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
setupCommitObjectViewer(toRefs(props))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -48,7 +48,7 @@
|
||||
v-if="colorBy"
|
||||
class="d-inline-block rounded mr-3 mt-1 elevation-3"
|
||||
:style="`width: 8px; height: 8px; background:${
|
||||
$store.state.colorLegend[type.fullName]
|
||||
viewerState.colorLegend[type.fullName]
|
||||
};`"
|
||||
></div>
|
||||
<v-btn
|
||||
@@ -60,7 +60,7 @@
|
||||
>
|
||||
<v-icon class="grey--text" style="font-size: 11px">
|
||||
{{
|
||||
$store.state.hideCategoryValues.indexOf(type.fullName) === -1
|
||||
viewerState.hideCategoryValues.indexOf(type.fullName) === -1
|
||||
? 'mdi-eye'
|
||||
: 'mdi-eye-off'
|
||||
}}
|
||||
@@ -75,14 +75,14 @@
|
||||
>
|
||||
<v-icon
|
||||
:class="`${
|
||||
$store.state.isolateCategoryValues.indexOf(type.fullName) !== -1
|
||||
viewerState.isolateCategoryValues.indexOf(type.fullName) !== -1
|
||||
? 'primary--text'
|
||||
: 'grey--text'
|
||||
}`"
|
||||
style="font-size: 11px"
|
||||
>
|
||||
{{
|
||||
!$store.state.isolateCategoryValues.indexOf(type.fullName) !== -1
|
||||
!viewerState.isolateCategoryValues.indexOf(type.fullName) !== -1
|
||||
? 'mdi-filter'
|
||||
: 'mdi-filter'
|
||||
}}
|
||||
@@ -93,6 +93,15 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import gql from 'graphql-tag'
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
hideCategoryToggle,
|
||||
isolateCategoryToggle,
|
||||
resetFilter,
|
||||
toggleColorByCategory
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
@@ -101,6 +110,25 @@ export default {
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
colorLegend
|
||||
hideCategoryValues
|
||||
isolateCategoryValues
|
||||
appliedFilter
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return {
|
||||
viewerState
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hidden: [],
|
||||
@@ -112,7 +140,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
colorBy() {
|
||||
return this.$store.state.appliedFilter && this.$store.state.appliedFilter.colorBy
|
||||
return this.viewerState.appliedFilter?.colorBy
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -124,7 +152,7 @@ export default {
|
||||
this.generateTypeMap(this.filter)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$store.commit('resetFilter')
|
||||
resetFilter()
|
||||
},
|
||||
methods: {
|
||||
mashColorLegend(colorLegend) {
|
||||
@@ -136,10 +164,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async toggleColors() {
|
||||
this.$store.commit('toggleColorByCategory', { filterKey: this.filter.targetKey })
|
||||
toggleColorByCategory({ filterKey: this.filter.targetKey })
|
||||
},
|
||||
async toggleFilter(type) {
|
||||
this.$store.commit('isolateCategoryToggle', {
|
||||
isolateCategoryToggle({
|
||||
colorBy: this.colorBy,
|
||||
filterKey: this.filter.targetKey,
|
||||
filterValue: type,
|
||||
@@ -147,7 +175,7 @@ export default {
|
||||
})
|
||||
},
|
||||
async toggleVisibility(type) {
|
||||
this.$store.commit('hideCategoryToggle', {
|
||||
hideCategoryToggle({
|
||||
colorBy: this.colorBy,
|
||||
filterKey: this.filter.targetKey,
|
||||
filterValue: type
|
||||
|
||||
@@ -79,7 +79,23 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {
|
||||
resetFilter,
|
||||
setNumericFilter
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HistogramSlider: async () => {
|
||||
await import(
|
||||
/* webpackChunkName: "vue-histogram-slider" */ 'vue-histogram-slider/dist/histogram-slider.css'
|
||||
)
|
||||
const component = await import(
|
||||
/* webpackChunkName: "vue-histogram-slider" */ 'vue-histogram-slider'
|
||||
)
|
||||
return component
|
||||
}
|
||||
},
|
||||
props: {
|
||||
filter: {
|
||||
type: Object,
|
||||
@@ -109,13 +125,13 @@ export default {
|
||||
this.$set(this.range, 0, this.filter.data.minValue)
|
||||
this.$set(this.range, 1, this.filter.data.maxValue)
|
||||
this.setFilter()
|
||||
this.width = this.$refs.parent.clientWidth - 24
|
||||
this.width = this.$refs.parent ? this.$refs.parent.clientWidth - 24 : 300
|
||||
this.$eventHub.$on('resize-viewer', () => {
|
||||
this.width = this.$refs.parent?.clientWidth - 24
|
||||
this.width = this.$refs.parent ? this.$refs.parent.clientWidth - 24 : 300
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$store.commit('resetFilter')
|
||||
resetFilter()
|
||||
},
|
||||
methods: {
|
||||
async setFilterHistogram(e) {
|
||||
@@ -123,7 +139,8 @@ export default {
|
||||
this.preventFirstSetInternal = false
|
||||
return
|
||||
}
|
||||
this.$store.commit('setNumericFilter', {
|
||||
|
||||
setNumericFilter({
|
||||
filterKey: this.filter.targetKey,
|
||||
minValue: e.from,
|
||||
maxValue: e.to
|
||||
@@ -134,7 +151,8 @@ export default {
|
||||
this.preventFirstSetInternal = false
|
||||
return
|
||||
}
|
||||
this.$store.commit('setNumericFilter', {
|
||||
|
||||
setNumericFilter({
|
||||
filterKey: this.filter.targetKey,
|
||||
minValue: this.range[0],
|
||||
maxValue: this.range[1]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card class="mx-2 mb-2 rounded-lg">
|
||||
<v-card class="mx-2 rounded-lg">
|
||||
<v-toolbar
|
||||
v-ripple
|
||||
class="transparent"
|
||||
@@ -14,7 +14,7 @@
|
||||
<v-chip small class="ma-1 caption grey white--text no-hover">Object</v-chip>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="$route.params.resourceId !== resource.id"
|
||||
v-if="resourceId !== resource.id"
|
||||
v-tooltip="'Remove'"
|
||||
small
|
||||
icon
|
||||
@@ -55,6 +55,16 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import gql from 'graphql-tag'
|
||||
import {
|
||||
hideObjects,
|
||||
isolateObjects,
|
||||
showObjects,
|
||||
unisolateObjects,
|
||||
useCommitObjectViewerParams
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
export default {
|
||||
components: {
|
||||
ObjectProperties: () => import('@/main/components/viewer/ObjectProperties')
|
||||
@@ -65,6 +75,22 @@ export default {
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId } = useCommitObjectViewerParams()
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
isolateValues
|
||||
hideValues
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewerState, streamId, resourceId }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false
|
||||
@@ -72,24 +98,22 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
isolated() {
|
||||
return (
|
||||
this.$store.state.isolateValues.indexOf(this.resource.data.object.id) !== -1
|
||||
)
|
||||
return this.viewerState.isolateValues.indexOf(this.resource.data.object.id) !== -1
|
||||
},
|
||||
visible() {
|
||||
return this.$store.state.hideValues.indexOf(this.resource.data.object.id) === -1
|
||||
return this.viewerState.hideValues.indexOf(this.resource.data.object.id) === -1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isolate() {
|
||||
const id = this.resource.data.object.id
|
||||
if (this.isolated)
|
||||
this.$store.commit('unisolateObjects', {
|
||||
unisolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: [id]
|
||||
})
|
||||
else
|
||||
this.$store.commit('isolateObjects', {
|
||||
isolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: [id]
|
||||
})
|
||||
@@ -97,12 +121,12 @@ export default {
|
||||
toggleVisibility() {
|
||||
const id = this.resource.data.object.id
|
||||
if (this.visible)
|
||||
this.$store.commit('hideObjects', {
|
||||
hideObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: [id]
|
||||
})
|
||||
else
|
||||
this.$store.commit('showObjects', {
|
||||
showObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: [id]
|
||||
})
|
||||
|
||||
@@ -124,6 +124,15 @@
|
||||
</template>
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import gql from 'graphql-tag'
|
||||
import {
|
||||
hideObjects,
|
||||
isolateObjects,
|
||||
showObjects,
|
||||
unisolateObjects
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -147,6 +156,21 @@ export default {
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
isolateValues
|
||||
hideValues
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewerState }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
@@ -168,11 +192,11 @@ export default {
|
||||
},
|
||||
visible() {
|
||||
if (this.prop.type === 'object') {
|
||||
return this.$store.state.hideValues.indexOf(this.prop.value.referencedId) === -1
|
||||
return this.viewerState.hideValues.indexOf(this.prop.value.referencedId) === -1
|
||||
}
|
||||
if (this.prop.type === 'array') {
|
||||
const ids = this.prop.value.map((o) => o.referencedId)
|
||||
const targetIds = this.$store.state.hideValues.filter(
|
||||
const targetIds = this.viewerState.hideValues.filter(
|
||||
(val) => ids.indexOf(val) !== -1
|
||||
)
|
||||
if (targetIds.length === 0) return true
|
||||
@@ -183,12 +207,12 @@ export default {
|
||||
isolated() {
|
||||
if (this.prop.type === 'object') {
|
||||
return (
|
||||
this.$store.state.isolateValues.indexOf(this.prop.value.referencedId) !== -1
|
||||
this.viewerState.isolateValues.indexOf(this.prop.value.referencedId) !== -1
|
||||
)
|
||||
}
|
||||
if (this.prop.type === 'array') {
|
||||
const ids = this.prop.value.map((o) => o.referencedId)
|
||||
const targetIds = this.$store.state.isolateValues.filter(
|
||||
const targetIds = this.viewerState.isolateValues.filter(
|
||||
(val) => ids.indexOf(val) !== -1
|
||||
)
|
||||
if (targetIds.length === 0) return false
|
||||
@@ -207,12 +231,12 @@ export default {
|
||||
}
|
||||
|
||||
if (this.visible)
|
||||
this.$store.commit('hideObjects', {
|
||||
hideObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: targetIds
|
||||
})
|
||||
else
|
||||
this.$store.commit('showObjects', {
|
||||
showObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: targetIds
|
||||
})
|
||||
@@ -225,12 +249,12 @@ export default {
|
||||
}
|
||||
|
||||
if (this.isolated)
|
||||
this.$store.commit('unisolateObjects', {
|
||||
unisolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: targetIds
|
||||
})
|
||||
else
|
||||
this.$store.commit('isolateObjects', {
|
||||
isolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: targetIds
|
||||
})
|
||||
|
||||
@@ -64,6 +64,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import gql from 'graphql-tag'
|
||||
import {
|
||||
isolateObjects,
|
||||
unisolateObjects
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
export default {
|
||||
components: {
|
||||
ObjectPropertiesRow: () => import('@/main/components/viewer/ObjectPropertiesRow')
|
||||
@@ -78,6 +85,20 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
isolateValues
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewerState }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expand: true
|
||||
@@ -101,7 +122,7 @@ export default {
|
||||
isolated() {
|
||||
const ids = this.objects.map((o) => o.id)
|
||||
ids.forEach((val) => {
|
||||
if (this.$store.state.isolateValues.indexOf(val) === -1) return false
|
||||
if (this.viewerState.isolateValues.indexOf(val) === -1) return false
|
||||
})
|
||||
return true
|
||||
}
|
||||
@@ -110,12 +131,12 @@ export default {
|
||||
isolateSelection() {
|
||||
const ids = this.objects.map((o) => o.id)
|
||||
if (!this.isolated)
|
||||
this.$store.commit('unisolateObjects', {
|
||||
unisolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: ids
|
||||
})
|
||||
else
|
||||
this.$store.commit('isolateObjects', {
|
||||
isolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: ids
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<div v-for="(resource, index) in resources" :key="index">
|
||||
<commit-info-resource
|
||||
v-if="resource.type === 'commit'"
|
||||
:class="[index === 0 ? 'my-2' : 'my-4']"
|
||||
:resource="resource"
|
||||
@remove="
|
||||
(e) => {
|
||||
@@ -15,6 +16,7 @@
|
||||
></commit-info-resource>
|
||||
<object-info-resource
|
||||
v-if="resource.type === 'object'"
|
||||
:class="[index === 0 ? 'my-2' : 'my-4']"
|
||||
:resource="resource"
|
||||
@remove="
|
||||
(e) => {
|
||||
@@ -24,7 +26,7 @@
|
||||
"
|
||||
></object-info-resource>
|
||||
</div>
|
||||
<div v-show="$loggedIn()" class="px-2 mb-2">
|
||||
<div v-if="allowAdd && isLoggedIn" class="px-2 mb-2">
|
||||
<v-btn
|
||||
v-tooltip="'Overlay another commit or object'"
|
||||
block
|
||||
@@ -71,13 +73,22 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useIsLoggedIn } from '@/main/lib/core/composables/auth'
|
||||
export default {
|
||||
components: {
|
||||
CommitInfoResource: () => import('@/main/components/viewer/CommitInfoResource'),
|
||||
ObjectInfoResource: () => import('@/main/components/viewer/ObjectInfoResource')
|
||||
},
|
||||
props: {
|
||||
resources: { type: Array, default: () => [] }
|
||||
resources: { type: Array, default: () => [] },
|
||||
allowAdd: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { isLoggedIn } = useIsLoggedIn()
|
||||
return { isLoggedIn }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
ref="parent"
|
||||
:style="`width: 100%; height: 100vh; position: absolute; pointer-events: none; overflow: hidden; opacity: ${
|
||||
$store.state.selectedComment || $store.state.addingComment ? '0.2' : '1'
|
||||
viewerState.selectedCommentMetaData || viewerState.addingComment ? '0.2' : '1'
|
||||
};`"
|
||||
>
|
||||
<div v-show="showBubbles">
|
||||
@@ -91,6 +91,14 @@ import * as THREE from 'three'
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
resetFilter,
|
||||
setFilterDirectly,
|
||||
useCommitObjectViewerParams
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default {
|
||||
name: 'ViewerBubbles',
|
||||
@@ -121,12 +129,12 @@ export default {
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
streamId: this.$route.params.streamId,
|
||||
resourceId: this.$route.params.resourceId
|
||||
streamId: this.streamId,
|
||||
resourceId: this.resourceId
|
||||
}
|
||||
},
|
||||
skip() {
|
||||
return !this.$route.params.resourceId || !this.$loggedIn()
|
||||
return !this.resourceId || !this.$loggedIn()
|
||||
},
|
||||
result(res) {
|
||||
const data = res.data
|
||||
@@ -177,6 +185,24 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { streamId, resourceId } = useCommitObjectViewerParams()
|
||||
const { viewer } = useInjectedViewer()
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
selectedCommentMetaData
|
||||
addingComment
|
||||
appliedFilter
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewer, viewerState, streamId, resourceId }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uuid: uuid(),
|
||||
@@ -198,7 +224,7 @@ export default {
|
||||
this.uuid = window.__bubblesId
|
||||
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
window.__viewer.cameraHandler.controls.addEventListener('update', () =>
|
||||
this.viewer.cameraHandler.controls.addEventListener('update', () =>
|
||||
this.updateBubbles(false)
|
||||
)
|
||||
|
||||
@@ -206,9 +232,7 @@ export default {
|
||||
window.addEventListener('beforeunload', async () => {
|
||||
await this.sendDisconnect()
|
||||
})
|
||||
this.resourceId = this.$route.params.resourceId
|
||||
window.__resourceId = this.$route.params.resourceId
|
||||
window.__viewer.on(
|
||||
this.viewer.on(
|
||||
'select',
|
||||
debounce((selectionInfo) => {
|
||||
this.selectedIds = selectionInfo.userData.map((o) => o.id)
|
||||
@@ -217,7 +241,7 @@ export default {
|
||||
this.sendUpdateAndPrune()
|
||||
}, 50)
|
||||
)
|
||||
window.__viewer.on('object-doubleclicked', () => {})
|
||||
this.viewer.on('object-doubleclicked', () => {})
|
||||
},
|
||||
async beforeDestroy() {
|
||||
await this.sendDisconnect()
|
||||
@@ -227,26 +251,27 @@ export default {
|
||||
setUserPow(user) {
|
||||
const camToSet = user.camera
|
||||
if (camToSet[6] === 1) {
|
||||
window.__viewer.toggleCameraProjection()
|
||||
this.viewer.toggleCameraProjection()
|
||||
}
|
||||
window.__viewer.interactions.setLookAt(
|
||||
this.viewer.interactions.setLookAt(
|
||||
{ x: camToSet[0], y: camToSet[1], z: camToSet[2] }, // position
|
||||
{ x: camToSet[3], y: camToSet[4], z: camToSet[5] } // target
|
||||
)
|
||||
if (camToSet[6] === 1) {
|
||||
window.__viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
|
||||
this.viewer.cameraHandler.activeCam.controls.zoom(camToSet[7], true)
|
||||
}
|
||||
if (user.filter) this.$store.commit('setFilterDirect', { filter: user.filter })
|
||||
else this.$store.commit('resetFilter')
|
||||
|
||||
if (user.filter) setFilterDirectly({ filter: user.filter })
|
||||
else resetFilter()
|
||||
|
||||
if (user.sectionBox) {
|
||||
window.__viewer.sectionBox.on()
|
||||
window.__viewer.sectionBox.setBox(user.sectionBox, 0)
|
||||
this.viewer.sectionBox.on()
|
||||
this.viewer.sectionBox.setBox(user.sectionBox, 0)
|
||||
}
|
||||
this.$mixpanel.track('Bubbles Action', { type: 'action', name: 'avatar-click' })
|
||||
},
|
||||
async sendUpdateAndPrune() {
|
||||
if (!this.$route.params.resourceId) return
|
||||
if (!this.resourceId) return
|
||||
for (const user of this.users) {
|
||||
const delta = Date.now() - user.lastUpdate
|
||||
if (delta > 20000) {
|
||||
@@ -262,7 +287,7 @@ export default {
|
||||
|
||||
if (!this.$loggedIn()) return
|
||||
|
||||
const controls = window.__viewer.cameraHandler.activeCam.controls
|
||||
const controls = this.viewer.cameraHandler.activeCam.controls
|
||||
const pos = controls.getPosition()
|
||||
const target = controls.getTarget()
|
||||
const c = [
|
||||
@@ -272,20 +297,20 @@ export default {
|
||||
parseFloat(target.x.toFixed(5)),
|
||||
parseFloat(target.y.toFixed(5)),
|
||||
parseFloat(target.z.toFixed(5)),
|
||||
window.__viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
|
||||
this.viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
|
||||
controls._zoom
|
||||
]
|
||||
|
||||
let selectionLocation = this.selectionLocation
|
||||
if (this.$store.state.selectedComment) {
|
||||
selectionLocation = this.$store.state.selectedComment.data.location
|
||||
if (this.viewerState.selectedCommentMetaData) {
|
||||
selectionLocation = this.viewerState.selectedCommentMetaData.selectionLocation
|
||||
}
|
||||
|
||||
const data = {
|
||||
filter: this.$store.state.appliedFilter,
|
||||
filter: this.viewerState.appliedFilter,
|
||||
selection: this.selectedIds,
|
||||
selectionLocation,
|
||||
sectionBox: window.__viewer.sectionBox.getCurrentBox(),
|
||||
sectionBox: this.viewer.sectionBox.getCurrentBox(),
|
||||
selectionCenter: this.selectionCenter,
|
||||
camera: c,
|
||||
userId: this.$userId(),
|
||||
@@ -294,7 +319,7 @@ export default {
|
||||
status: 'viewing'
|
||||
}
|
||||
|
||||
if (!this.$route.params.streamId) return
|
||||
if (!this.streamId) return
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation userViewerActivityBroadcast(
|
||||
@@ -310,15 +335,15 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.$route.params.streamId,
|
||||
resourceId: this.$route.params.resourceId,
|
||||
streamId: this.streamId,
|
||||
resourceId: this.resourceId,
|
||||
data
|
||||
}
|
||||
})
|
||||
},
|
||||
async sendDisconnect() {
|
||||
if (!this.$loggedIn()) return
|
||||
if (!this.$route.params.streamId) return
|
||||
if (!this.streamId) return
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
@@ -335,17 +360,16 @@ export default {
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
streamId: this.$route.params.streamId,
|
||||
resourceId: this.resourceId || window.__resourceId,
|
||||
streamId: this.streamId,
|
||||
resourceId: this.resourceId,
|
||||
data: { userId: this.$userId(), uuid: this.uuid, status: 'disconnect' }
|
||||
}
|
||||
})
|
||||
delete window.__resourceId
|
||||
},
|
||||
updateBubbles(transition = true) {
|
||||
if (!this.$refs.parent) return
|
||||
|
||||
const cam = window.__viewer.cameraHandler.camera
|
||||
const cam = this.viewer.cameraHandler.camera
|
||||
cam.updateProjectionMatrix()
|
||||
const selectedObjects = []
|
||||
for (const user of this.users) {
|
||||
@@ -442,7 +466,7 @@ export default {
|
||||
uArrowEl.style.opacity = user.clipped ? '0' : '1'
|
||||
}
|
||||
|
||||
window.__viewer.interactions.overlayObjects(selectedObjects)
|
||||
this.viewer.interactions.overlayObjects(selectedObjects)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import gql from 'graphql-tag'
|
||||
import { resetFilter } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
export default {
|
||||
components: {
|
||||
CanonicalViews: () => import('@/main/components/viewer/CanonicalViews')
|
||||
@@ -65,6 +70,21 @@ export default {
|
||||
props: {
|
||||
small: { type: Boolean, default: false }
|
||||
},
|
||||
setup() {
|
||||
const { viewer } = useInjectedViewer()
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
appliedFilter
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewer, viewerState }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fullScreen: false
|
||||
@@ -72,7 +92,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
showVisReset() {
|
||||
return !!this.$store.state.appliedFilter
|
||||
return !!this.viewerState.appliedFilter
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -80,16 +100,16 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
toggleCamera() {
|
||||
window.__viewer.toggleCameraProjection()
|
||||
this.viewer.toggleCameraProjection()
|
||||
},
|
||||
resetVisibility() {
|
||||
this.$store.commit('resetFilter')
|
||||
resetFilter()
|
||||
},
|
||||
zoomEx() {
|
||||
window.__viewer.zoomExtents()
|
||||
this.viewer.zoomExtents()
|
||||
},
|
||||
sectionToggle() {
|
||||
window.__viewer.toggleSectionBox()
|
||||
this.viewer.toggleSectionBox()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,9 @@
|
||||
</v-list>
|
||||
</template>
|
||||
<script>
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import gql from 'graphql-tag'
|
||||
import { computed } from 'vue'
|
||||
export default {
|
||||
name: 'ViewerFilters',
|
||||
components: {
|
||||
@@ -107,6 +110,20 @@ export default {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
appliedFilter
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return { viewerState }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expand: false,
|
||||
@@ -149,10 +166,10 @@ export default {
|
||||
this.parseAndSetFilters()
|
||||
}
|
||||
},
|
||||
'$store.state.appliedFilter'() {
|
||||
'viewerState.appliedFilter'() {
|
||||
if (this.trySetPresetFilter) return
|
||||
if (this.$store.state.appliedFilter && this.$store.state.appliedFilter.filterBy) {
|
||||
const key = Object.keys(this.$store.state.appliedFilter.filterBy)[0]
|
||||
if (this.viewerState.appliedFilter?.filterBy) {
|
||||
const key = Object.keys(this.viewerState.appliedFilter.filterBy)[0]
|
||||
const presetFilter = this.allFilters.find((f) => f.targetKey === key)
|
||||
if (presetFilter) this.activeFilter = presetFilter
|
||||
this.trySetPresetFilter = true
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
</v-list>
|
||||
</template>
|
||||
<script>
|
||||
import { useInjectedViewer } from '@/main/lib/viewer/core/composables/viewer'
|
||||
export default {
|
||||
props: {
|
||||
views: {
|
||||
@@ -44,6 +45,10 @@ export default {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const { viewer } = useInjectedViewer()
|
||||
return { viewer }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expand: false
|
||||
@@ -51,7 +56,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
setView(view) {
|
||||
window.__viewer.interactions.setView(view.id)
|
||||
this.viewer.interactions.setView(view.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ export default {
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
apollo: {},
|
||||
data() {
|
||||
return {
|
||||
items: ['All Commits'],
|
||||
@@ -158,7 +157,7 @@ export default {
|
||||
}
|
||||
if (pcs.length !== 1) {
|
||||
const streamId = pcs[2]
|
||||
if (streamId !== this.$route.params.streamId) {
|
||||
if (streamId !== this.streamId) {
|
||||
this.objectIdError = 'Objects do not belong to the same stream.'
|
||||
this.objectId = null
|
||||
return
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="embed-viewer-core">
|
||||
<!-- Top bar (menu toggle + powered by speckle text) -->
|
||||
<div
|
||||
class="embed-viewer-core__top-bar top-left bottom-left pa-4 d-flex justify-space-between"
|
||||
style="right: 0px; position: fixed; z-index: 5; width: 100%"
|
||||
>
|
||||
<v-btn fab small style="z-index=1000" @click="drawer = !drawer">
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</v-btn>
|
||||
<span v-show="!drawer" class="caption d-inline-flex align-center">
|
||||
<img src="@/assets/logo.svg" height="18" />
|
||||
<span style="margin-top: 2px" class="primary--text">
|
||||
<a href="https://speckle.xyz" target="_blank" class="text-decoration-none">
|
||||
<b>Powered by Speckle</b>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Viewer filters panel / sidebar -->
|
||||
<v-navigation-drawer
|
||||
ref="drawerRef"
|
||||
v-model="drawer"
|
||||
class="viewer-controls-drawer"
|
||||
app
|
||||
floating
|
||||
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} elevation-1`"
|
||||
:width="navWidth"
|
||||
disable-resize-watcher
|
||||
style="z-index: 100"
|
||||
>
|
||||
<div class="px-1 pt-1 d-flex flex-column" style="height: 100%; width: 100%">
|
||||
<!-- Drawer closer -->
|
||||
<v-btn icon small class="align-self-end mb-2" @click="drawer = false">
|
||||
<v-icon x-small>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- Sidebar portal -->
|
||||
<portal-target name="nav" />
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- Actual viewer -->
|
||||
<div class="embed-viewer-core__viewer viewer-wrapper no-scrollbar">
|
||||
<commit-object-viewer
|
||||
:stream-id="streamId"
|
||||
:resource-id="resourceId"
|
||||
:is-embed="true"
|
||||
@models-loaded="onModelsLoaded"
|
||||
/>
|
||||
</div>
|
||||
<!-- Appending buttons to viewercontrols (these should be ordered last) -->
|
||||
<portal to="viewercontrols" :order="100">
|
||||
<v-btn
|
||||
v-tooltip="'View extra details in Speckle!'"
|
||||
icon
|
||||
dark
|
||||
large
|
||||
class="elevation-5 primary pa-0 ma-o"
|
||||
:href="goToServerUrl"
|
||||
target="blank"
|
||||
>
|
||||
<v-icon dark small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import Vue, { computed, defineComponent, Ref, ref } from 'vue'
|
||||
import CommitObjectViewer from '@/main/pages/stream/CommitObjectViewer.vue'
|
||||
import { useEmbedViewerQuery } from '@/main/lib/viewer/commit-object-viewer/composables/embed'
|
||||
import { useNavigationDrawerAutoResize } from '@/main/lib/core/composables/dom'
|
||||
import { Nullable } from '@/helpers/typeHelpers'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EmbeddedCommitObjectViewer',
|
||||
components: {
|
||||
CommitObjectViewer
|
||||
},
|
||||
props: {
|
||||
streamId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
resourceId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(_, { emit }) {
|
||||
const drawerRef: Ref<Nullable<Vue>> = ref(null)
|
||||
const loadedModel = ref(false)
|
||||
const drawer = ref(false)
|
||||
|
||||
const { navWidth } = useNavigationDrawerAutoResize({
|
||||
drawerRef
|
||||
})
|
||||
|
||||
const { streamId, commitId, objectId, branchName } = useEmbedViewerQuery()
|
||||
const goToServerUrl = computed(() => {
|
||||
const base = `${window.location.origin}/streams/${streamId.value}/`
|
||||
|
||||
if (commitId.value) return base + `commits/${commitId.value}`
|
||||
if (objectId.value) return base + `objects/${objectId.value}`
|
||||
if (branchName.value) return base + `branches/${encodeURI(branchName.value)}`
|
||||
|
||||
return base
|
||||
})
|
||||
return {
|
||||
goToServerUrl,
|
||||
loadedModel,
|
||||
drawer,
|
||||
// drawer ref must be returned, for it to be filled
|
||||
drawerRef,
|
||||
navWidth,
|
||||
onModelsLoaded: () => {
|
||||
loadedModel.value = true
|
||||
emit('models-loaded')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.embed-viewer-core {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&__viewer {
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
export function getCamArray() {
|
||||
const controls = window.__viewer.cameraHandler.activeCam.controls
|
||||
const pos = controls.getPosition()
|
||||
const target = controls.getTarget()
|
||||
const c = [
|
||||
parseFloat(pos.x.toFixed(5)),
|
||||
parseFloat(pos.y.toFixed(5)),
|
||||
parseFloat(pos.z.toFixed(5)),
|
||||
parseFloat(target.x.toFixed(5)),
|
||||
parseFloat(target.y.toFixed(5)),
|
||||
parseFloat(target.z.toFixed(5)),
|
||||
window.__viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
|
||||
controls._zoom
|
||||
]
|
||||
return c
|
||||
}
|
||||
@@ -114,6 +114,7 @@
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { userSearchQuery } from '@/graphql/user'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -180,7 +181,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
addCollab(user) {
|
||||
if (user.id === localStorage.getItem('uuid')) return
|
||||
if (user.id === AppLocalStorage.get('uuid')) return
|
||||
const indx = this.collabs.findIndex((u) => u.id === user.id)
|
||||
if (indx !== -1) return
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<v-app id="speckle">
|
||||
<v-main class="background">
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
<global-toast />
|
||||
<global-loading />
|
||||
</v-app>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
/**
|
||||
* A very basic layout that does the core required setup, but doesn't add any navbars, menus or anything. Can be used
|
||||
* for embed or other simplified pages.
|
||||
*/
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TheBasic',
|
||||
components: {
|
||||
GlobalToast: () => import('@/main/components/common/GlobalToast.vue'),
|
||||
GlobalLoading: () => import('@/main/components/common/GlobalLoading.vue')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-app id="speckle">
|
||||
<v-navigation-drawer
|
||||
ref="drawer"
|
||||
ref="navDrawer"
|
||||
v-model="drawer"
|
||||
app
|
||||
floating
|
||||
@@ -63,7 +63,8 @@
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { mainUserDataQuery } from '@/graphql/user'
|
||||
import { setDarkTheme } from '@/main/utils/themeStateManager'
|
||||
import { useNavigationDrawerAutoResize } from '../lib/core/composables/dom'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'TheMain',
|
||||
@@ -107,33 +108,35 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const navDrawer = ref(null)
|
||||
|
||||
const { navWidth } = useNavigationDrawerAutoResize({
|
||||
drawerRef: navDrawer
|
||||
})
|
||||
|
||||
// drawer ref must be returned, for it to be filled
|
||||
return {
|
||||
navDrawer,
|
||||
navWidth
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newStreamDialog: 1,
|
||||
drawer: true,
|
||||
navWidth: 300,
|
||||
navRestWidth: 300,
|
||||
borderSize: 3,
|
||||
hideEmailBanner: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler(to) {
|
||||
if (!to.meta.resizableNavbar) {
|
||||
this.navWidth = this.navRestWidth
|
||||
}
|
||||
if (to.meta.resizableNavbar && window.__lastNavSize) {
|
||||
this.navWidth = window.__lastNavSize
|
||||
}
|
||||
this.hideEmailBanner = !!to.meta.hideEmailBanner
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setNavResizeEvents()
|
||||
|
||||
if (this.$route.query.emailverfiedstatus) {
|
||||
setTimeout(() => {
|
||||
this.$eventHub.$emit('notification', {
|
||||
@@ -141,78 +144,10 @@ export default {
|
||||
})
|
||||
}, 1000) // todo: ask fabian if there's a better way, feels icky
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchTheme() {
|
||||
this.$vuetify.theme.dark = !this.$vuetify.theme.dark
|
||||
setDarkTheme(this.$vuetify.theme.dark, true)
|
||||
|
||||
this.$mixpanel.people.set(
|
||||
'Theme Web',
|
||||
this.$vuetify.theme.dark ? 'dark' : 'light'
|
||||
)
|
||||
},
|
||||
setNavResizeEvents() {
|
||||
const minSize = this.borderSize
|
||||
const el = this.$refs.drawer.$el
|
||||
const drawerBorder = el.querySelector('.nav-resizer')
|
||||
drawerBorder.style.cursor = 'ew-resize'
|
||||
|
||||
function resize(e) {
|
||||
e.preventDefault()
|
||||
const maxWidth = document.body.offsetWidth / 2
|
||||
const minWidth = 300
|
||||
document.body.style.cursor = 'ew-resize'
|
||||
if (!(e.clientX > maxWidth || e.clientX < minWidth)) {
|
||||
el.style.width = e.clientX + 'px'
|
||||
window.__lastNavSize = e.clientX
|
||||
}
|
||||
}
|
||||
|
||||
drawerBorder.addEventListener(
|
||||
'mousedown',
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
if (e.offsetX < minSize) {
|
||||
el.style.transition = 'initial'
|
||||
document.addEventListener('mousemove', resize, false)
|
||||
}
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
document.addEventListener(
|
||||
'mouseup',
|
||||
(e) => {
|
||||
e.preventDefault()
|
||||
el.style.transition = ''
|
||||
document.body.style.cursor = ''
|
||||
this.navWidth = el.style.width
|
||||
document.removeEventListener('mousemove', resize, false)
|
||||
setTimeout(() => this.$eventHub.$emit('resize-viewer'), 300)
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.nav-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
z-index: 100000;
|
||||
transition: all 0.6s ease;
|
||||
opacity: 0.01;
|
||||
border: 4px solid royalblue;
|
||||
}
|
||||
.nav-resizer:hover {
|
||||
opacity: 0.5;
|
||||
width: 0px;
|
||||
}
|
||||
.email-banner {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IsLoggedInDocument } from '@/graphql/generated/graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
|
||||
/**
|
||||
* Composable that resolves whether the user is logged in through an Apollo query
|
||||
*/
|
||||
export function useIsLoggedIn() {
|
||||
const { result } = useQuery(IsLoggedInDocument)
|
||||
const isLoggedIn = computed(() => !!result.value?.user?.id)
|
||||
return { isLoggedIn }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ComposableInvokedOutOfScopeError } from '@/main/lib/core/errors/composition'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
/**
|
||||
* Get EventHub
|
||||
*/
|
||||
export function useEventHub() {
|
||||
const vm = getCurrentInstance()
|
||||
if (!vm) throw new ComposableInvokedOutOfScopeError()
|
||||
|
||||
return vm.proxy.$eventHub
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useEventHub } from '@/main/lib/core/composables/core'
|
||||
import { useRoute } from '@/main/lib/core/composables/router'
|
||||
import { onBeforeUnmount, onMounted, Ref, ref, unref, watch } from 'vue'
|
||||
|
||||
/**
|
||||
* Sets up v-navigation-drawer autoresize (ask @dim how this works)
|
||||
*/
|
||||
export function useNavigationDrawerAutoResize(params: {
|
||||
navRestWidth?: number
|
||||
borderSize?: number
|
||||
drawerRef: Ref<Vue | null> | Vue
|
||||
}) {
|
||||
const { navRestWidth = 300, borderSize = 3, drawerRef } = params
|
||||
const navWidth = ref<string | number>(navRestWidth)
|
||||
const minSize = borderSize
|
||||
|
||||
const route = useRoute()
|
||||
const eventHub = useEventHub()
|
||||
const drawerEl = () => {
|
||||
const comp = unref(drawerRef)
|
||||
if (!comp)
|
||||
throw new Error(
|
||||
"Couldn't resolve drawer ref! Is it returned from the setup function?"
|
||||
)
|
||||
|
||||
return comp.$el as HTMLElement
|
||||
}
|
||||
|
||||
// Adjust navigation width according to route metadata
|
||||
watch(
|
||||
() => route,
|
||||
(to) => {
|
||||
if (!to.meta?.resizableNavbar) {
|
||||
navWidth.value = navRestWidth
|
||||
}
|
||||
if (to.meta?.resizableNavbar && window.__lastNavSize) {
|
||||
navWidth.value = window.__lastNavSize
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function resizeHandler(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
const el = drawerEl()
|
||||
|
||||
const maxWidth = document.body.offsetWidth / 2
|
||||
const minWidth = 300
|
||||
|
||||
document.body.style.cursor = 'ew-resize'
|
||||
if (!(e.clientX > maxWidth || e.clientX < minWidth)) {
|
||||
el.style.width = e.clientX + 'px'
|
||||
window.__lastNavSize = e.clientX
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
const el = drawerEl()
|
||||
|
||||
if (e.offsetX < minSize) {
|
||||
el.style.transition = 'initial'
|
||||
document.addEventListener('mousemove', resizeHandler, false)
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
const el = drawerEl()
|
||||
|
||||
el.style.transition = ''
|
||||
document.body.style.cursor = ''
|
||||
navWidth.value = el.style.width
|
||||
document.removeEventListener('mousemove', resizeHandler, false)
|
||||
setTimeout(() => eventHub.$emit('resize-viewer'), 300)
|
||||
}
|
||||
|
||||
// Setup resize events
|
||||
onMounted(() => {
|
||||
const el = drawerEl()
|
||||
const drawerBorder = el.querySelector<HTMLElement>('.nav-resizer')
|
||||
if (drawerBorder) {
|
||||
drawerBorder.style.cursor = 'ew-resize'
|
||||
drawerBorder.addEventListener('mousedown', onMouseDown, false)
|
||||
}
|
||||
|
||||
document.addEventListener('mouseup', onMouseUp, false)
|
||||
})
|
||||
|
||||
// Unsub events
|
||||
onBeforeUnmount(() => {
|
||||
const el = drawerEl()
|
||||
const drawerBorder = el.querySelector<HTMLElement>('.nav-resizer')
|
||||
|
||||
if (drawerBorder) {
|
||||
drawerBorder.removeEventListener('mousedown', onMouseDown)
|
||||
}
|
||||
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('mousemove', resizeHandler)
|
||||
})
|
||||
|
||||
return {
|
||||
navWidth
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ComposableInvokedOutOfScopeError } from '@/main/lib/core/errors/composition'
|
||||
import { OverridedMixpanel } from 'mixpanel-browser'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
/**
|
||||
* Get Mixpanel instance (not reactive)
|
||||
*/
|
||||
export function useMixpanel(): OverridedMixpanel {
|
||||
const vm = getCurrentInstance()
|
||||
if (!vm) throw new ComposableInvokedOutOfScopeError()
|
||||
|
||||
return vm.proxy.$mixpanel
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useRoute } from '@/main/lib/core/composables/router'
|
||||
import { computed } from 'vue'
|
||||
|
||||
/**
|
||||
* Get embed viewer query params
|
||||
*/
|
||||
export function useEmbedViewerQuery() {
|
||||
const route = useRoute()
|
||||
const streamId = computed(() => (route.query.stream as string) || null)
|
||||
const branchName = computed(() => (route.query.branch as string) || null)
|
||||
const commitId = computed(() => (route.query.commit as string) || null)
|
||||
const objectId = computed(() => (route.query.object as string) || null)
|
||||
const transparent = computed(() => route.query.transparent === 'true')
|
||||
|
||||
return { streamId, branchName, commitId, objectId, transparent }
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
import { GetReactiveVarType, Nullable } from '@/helpers/typeHelpers'
|
||||
import { setupNewViewerInjection } from '@/main/lib/viewer/core/composables/viewer'
|
||||
import { makeVar, TypePolicies } from '@apollo/client/cache'
|
||||
import { DefaultViewerParams, Viewer } from '@speckle/viewer'
|
||||
import emojis from '@/main/store/emojis'
|
||||
import { has, isArray } from 'lodash'
|
||||
import { computed, ComputedRef, inject, InjectionKey, provide, Ref } from 'vue'
|
||||
|
||||
const ViewerStreamIdKey: InjectionKey<Ref<string>> = Symbol(
|
||||
'COMMIT_OBJECT_VIEWER_STREAMID'
|
||||
)
|
||||
const ViewerResourceIdKey: InjectionKey<Ref<string>> = Symbol(
|
||||
'COMMIT_OBJECT_VIEWER_RESOURCEID'
|
||||
)
|
||||
const ViewerIsEmbedKey: InjectionKey<Ref<boolean>> = Symbol(
|
||||
'COMMIT_OBJECT_VIEWER_IS_EMBED'
|
||||
)
|
||||
|
||||
type UnknownObject = Record<string, unknown>
|
||||
|
||||
type GlobalViewerData = {
|
||||
viewer: Viewer
|
||||
container: HTMLElement
|
||||
initialized: Promise<void>
|
||||
}
|
||||
|
||||
type FilterKeyAndValues = {
|
||||
filterKey: string
|
||||
filterValues: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Global CommitObjectViewer viewer instance & container. It's not held in commitObjectViewerState, because it's a
|
||||
* complex object that keeps mutating itself (triggering Apollo Client errors)
|
||||
*/
|
||||
let globalViewerData: Nullable<GlobalViewerData> = null
|
||||
|
||||
/**
|
||||
* Queryable Apollo Client state
|
||||
*/
|
||||
const commitObjectViewerState = makeVar({
|
||||
viewerBusy: false,
|
||||
appliedFilter: null as Nullable<UnknownObject>,
|
||||
isolateKey: null as Nullable<string>,
|
||||
isolateValues: [] as string[],
|
||||
hideKey: null as Nullable<string>,
|
||||
hideValues: [] as string[],
|
||||
colorLegend: {} as UnknownObject,
|
||||
isolateCategoryKey: null as Nullable<string>,
|
||||
isolateCategoryValues: [] as string[],
|
||||
hideCategoryKey: null as Nullable<string>,
|
||||
hideCategoryValues: [] as string[],
|
||||
selectedCommentMetaData: null as Nullable<{
|
||||
id: number
|
||||
selectionLocation: Record<string, unknown>
|
||||
}>,
|
||||
addingComment: false,
|
||||
preventCommentCollapse: false,
|
||||
commentReactions: ['❤️', '✏️', '🔥', '⚠️'],
|
||||
emojis
|
||||
})
|
||||
|
||||
export type StateType = GetReactiveVarType<typeof commitObjectViewerState>
|
||||
|
||||
/**
|
||||
* Merge (through _.merge) these with the rest of your Apollo Client `typePolicies` to set up
|
||||
* commit object viewer state management
|
||||
*/
|
||||
export const statePolicies: TypePolicies = {
|
||||
Query: {
|
||||
fields: {
|
||||
commitObjectViewerState: {
|
||||
read() {
|
||||
return commitObjectViewerState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current global Commit Object Viewer instance or create one
|
||||
*/
|
||||
function getOrInitViewerData(): GlobalViewerData {
|
||||
if (globalViewerData) return globalViewerData
|
||||
|
||||
const container = document.createElement('div')
|
||||
container.id = 'renderer'
|
||||
container.className = 'viewer-container'
|
||||
container.style.display = 'inline-block'
|
||||
|
||||
const viewer = new Viewer(container, DefaultViewerParams)
|
||||
const initPromise = viewer.init()
|
||||
|
||||
globalViewerData = {
|
||||
viewer,
|
||||
container,
|
||||
initialized: initPromise
|
||||
}
|
||||
|
||||
return globalViewerData
|
||||
}
|
||||
|
||||
function getInitializedViewer(): Viewer {
|
||||
if (!globalViewerData?.viewer) {
|
||||
throw new Error('Attempting to access viewer before it has been initialized')
|
||||
}
|
||||
|
||||
return globalViewerData.viewer
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable that returns the global Commit Object Viewer instance and injects it into child components
|
||||
*/
|
||||
export function setupCommitObjectViewer(reactiveMainProps: {
|
||||
streamId: Ref<string>
|
||||
resourceId: Ref<string>
|
||||
isEmbed: Ref<boolean>
|
||||
}) {
|
||||
const { streamId, resourceId, isEmbed } = reactiveMainProps
|
||||
|
||||
// Set up and inject viewer
|
||||
const viewerData = getOrInitViewerData()
|
||||
const { viewer, container, isInitialized, isInitializedPromise } =
|
||||
setupNewViewerInjection({
|
||||
viewer: viewerData.viewer,
|
||||
container: viewerData.container,
|
||||
initPromise: viewerData.initialized
|
||||
})
|
||||
|
||||
// Inject main parameters into child components
|
||||
provide(ViewerStreamIdKey, streamId)
|
||||
provide(ViewerResourceIdKey, resourceId)
|
||||
provide(ViewerIsEmbedKey, isEmbed)
|
||||
|
||||
return { viewer, container, isInitialized, isInitializedPromise }
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the Commit Object Viewer instance's main parameters
|
||||
*/
|
||||
export function useCommitObjectViewerParams() {
|
||||
const injectedStreamId = inject(ViewerStreamIdKey)
|
||||
const injectedResourceId = inject(ViewerResourceIdKey)
|
||||
const injectedIsEmbed = inject(ViewerIsEmbedKey)
|
||||
|
||||
const buildSafeRef = <T>(ref: Ref<T> | undefined): ComputedRef<T> =>
|
||||
computed(() => {
|
||||
if (!ref) {
|
||||
throw new Error(
|
||||
"Couldn't resolve Commit Object Viewer injected state! Is it properly set up??"
|
||||
)
|
||||
}
|
||||
|
||||
return ref.value
|
||||
})
|
||||
|
||||
const streamId = buildSafeRef(injectedStreamId)
|
||||
const resourceId = buildSafeRef(injectedResourceId)
|
||||
const isEmbed = buildSafeRef(injectedIsEmbed)
|
||||
|
||||
return { streamId, resourceId, isEmbed }
|
||||
}
|
||||
|
||||
/*
|
||||
* STATE MODIFICATION FUNCTIONS:
|
||||
*/
|
||||
|
||||
function updateState(newValues: Partial<StateType>) {
|
||||
commitObjectViewerState({
|
||||
...commitObjectViewerState(),
|
||||
...newValues
|
||||
})
|
||||
}
|
||||
|
||||
export function setIsViewerBusy(isBusy: boolean) {
|
||||
updateState({ viewerBusy: isBusy })
|
||||
}
|
||||
|
||||
export function setIsAddingComment(isAddingComment: boolean) {
|
||||
updateState({ addingComment: isAddingComment })
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: We should not set the entire comment here, because we mutate comments in multiple places
|
||||
* and that would cause a cache mutation
|
||||
*/
|
||||
export function setSelectedCommentMetaData(
|
||||
comment: { id: number; data: { location: Record<string, unknown> } } | null
|
||||
) {
|
||||
updateState({
|
||||
selectedCommentMetaData: comment
|
||||
? { id: comment.id, selectionLocation: comment.data.location }
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
export function isolateObjects(params: FilterKeyAndValues) {
|
||||
const { filterKey, filterValues } = params
|
||||
|
||||
const state = { ...commitObjectViewerState() }
|
||||
state.hideKey = null
|
||||
state.hideValues = []
|
||||
if (state.isolateKey !== filterKey) {
|
||||
state.isolateValues = []
|
||||
}
|
||||
|
||||
state.isolateKey = filterKey
|
||||
state.isolateValues = [...new Set([...state.isolateValues, ...filterValues])]
|
||||
if (state.isolateValues.length === 0) {
|
||||
state.appliedFilter = null
|
||||
} else {
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { includes: state.isolateValues } },
|
||||
ghostOthers: true
|
||||
}
|
||||
}
|
||||
|
||||
updateState(state)
|
||||
getInitializedViewer().applyFilter(state.appliedFilter)
|
||||
}
|
||||
|
||||
export function unisolateObjects(params: FilterKeyAndValues) {
|
||||
const { filterKey, filterValues } = params
|
||||
|
||||
const state = { ...commitObjectViewerState() }
|
||||
state.hideKey = null
|
||||
state.hideValues = []
|
||||
if (state.isolateKey !== filterKey) {
|
||||
state.isolateValues = []
|
||||
}
|
||||
|
||||
state.isolateKey = filterKey
|
||||
state.isolateValues = state.isolateValues.filter(
|
||||
(val) => filterValues.indexOf(val) === -1
|
||||
)
|
||||
if (state.isolateValues.length === 0) {
|
||||
state.appliedFilter = null
|
||||
} else {
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { includes: state.isolateValues } },
|
||||
ghostOthers: true
|
||||
}
|
||||
}
|
||||
|
||||
updateState(state)
|
||||
getInitializedViewer().applyFilter(state.appliedFilter)
|
||||
}
|
||||
|
||||
export function hideObjects(params: FilterKeyAndValues) {
|
||||
const { filterKey, filterValues } = params
|
||||
const state = { ...commitObjectViewerState() }
|
||||
|
||||
state.isolateKey = null
|
||||
state.isolateValues = []
|
||||
if (state.hideKey !== filterKey) {
|
||||
state.hideValues = []
|
||||
}
|
||||
|
||||
state.hideKey = filterKey
|
||||
state.hideValues = [...new Set([...filterValues, ...state.hideValues])]
|
||||
|
||||
if (state.hideValues.length === 0) {
|
||||
state.appliedFilter = null
|
||||
} else {
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { excludes: state.hideValues } }
|
||||
}
|
||||
}
|
||||
|
||||
updateState(state)
|
||||
getInitializedViewer().applyFilter(state.appliedFilter)
|
||||
}
|
||||
|
||||
export function showObjects(params: FilterKeyAndValues) {
|
||||
const { filterKey, filterValues } = params
|
||||
const state = { ...commitObjectViewerState() }
|
||||
|
||||
state.isolateKey = null
|
||||
state.isolateValues = []
|
||||
if (state.hideKey !== filterKey) {
|
||||
state.hideValues = []
|
||||
}
|
||||
|
||||
state.hideKey = filterKey
|
||||
state.hideValues = state.hideValues.filter((val) => filterValues.indexOf(val) === -1)
|
||||
|
||||
if (state.hideValues.length === 0) {
|
||||
state.appliedFilter = null
|
||||
} else {
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { excludes: state.hideValues } }
|
||||
}
|
||||
}
|
||||
|
||||
updateState(state)
|
||||
getInitializedViewer().applyFilter(state.appliedFilter)
|
||||
}
|
||||
|
||||
function resetInternalHideIsolateObjectState() {
|
||||
updateState({
|
||||
isolateKey: null,
|
||||
isolateValues: [],
|
||||
hideKey: null,
|
||||
hideValues: []
|
||||
})
|
||||
}
|
||||
|
||||
export async function isolateCategoryToggle(params: {
|
||||
filterKey: string
|
||||
filterValue: string
|
||||
allValues: string[]
|
||||
colorBy?: Record<string, unknown> | false
|
||||
}) {
|
||||
resetInternalHideIsolateObjectState()
|
||||
|
||||
const { filterKey, filterValue, allValues, colorBy = false } = params
|
||||
const state = { ...commitObjectViewerState() }
|
||||
const viewer = getInitializedViewer()
|
||||
|
||||
state.hideCategoryKey = null
|
||||
state.hideCategoryValues = []
|
||||
|
||||
if (filterKey !== state.isolateCategoryKey) {
|
||||
state.isolateCategoryValues = []
|
||||
}
|
||||
state.isolateCategoryKey = filterKey
|
||||
|
||||
const isolateCategoryValues = [...state.isolateCategoryValues]
|
||||
const indx = isolateCategoryValues.indexOf(filterValue)
|
||||
if (indx === -1) {
|
||||
isolateCategoryValues.push(filterValue)
|
||||
} else {
|
||||
isolateCategoryValues.splice(indx, 1)
|
||||
}
|
||||
state.isolateCategoryValues = isolateCategoryValues
|
||||
|
||||
if (
|
||||
(state.isolateCategoryValues.length === 0 ||
|
||||
state.isolateCategoryValues.length === allValues.length) &&
|
||||
!colorBy
|
||||
) {
|
||||
state.appliedFilter = null
|
||||
updateState(state)
|
||||
viewer.applyFilter(state.appliedFilter)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.isolateCategoryValues.length === 0 && colorBy) {
|
||||
state.appliedFilter = {
|
||||
colorBy: { type: 'category', property: filterKey }
|
||||
}
|
||||
}
|
||||
if (state.isolateCategoryValues.length !== 0) {
|
||||
state.appliedFilter = {
|
||||
ghostOthers: true,
|
||||
filterBy: { [filterKey]: state.isolateCategoryValues },
|
||||
colorBy: colorBy ? { type: 'category', property: filterKey } : null
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
state.isolateCategoryValues.length === allValues.length &&
|
||||
state.appliedFilter?.filterBy
|
||||
) {
|
||||
const newAppliedFilter = { ...state.appliedFilter }
|
||||
delete newAppliedFilter.filterBy
|
||||
|
||||
state.appliedFilter = newAppliedFilter
|
||||
}
|
||||
|
||||
const res = await viewer.applyFilter(state.appliedFilter)
|
||||
state.colorLegend = res.colorLegend
|
||||
updateState(state)
|
||||
}
|
||||
|
||||
export async function hideCategoryToggle(params: {
|
||||
filterKey: string
|
||||
filterValue: string
|
||||
colorBy?: Record<string, unknown> | false
|
||||
}) {
|
||||
resetInternalHideIsolateObjectState()
|
||||
|
||||
const { filterKey, filterValue, colorBy = false } = params
|
||||
const state = { ...commitObjectViewerState() }
|
||||
const viewer = getInitializedViewer()
|
||||
|
||||
state.isolateCategoryKey = null
|
||||
state.isolateCategoryValues = []
|
||||
if (filterKey !== state.hideCategoryKey) {
|
||||
state.hideCategoryValues = []
|
||||
}
|
||||
state.hideCategoryKey = filterKey
|
||||
|
||||
const hideCategoryValues = [...state.hideCategoryValues]
|
||||
const indx = hideCategoryValues.indexOf(filterValue)
|
||||
if (indx === -1) {
|
||||
hideCategoryValues.push(filterValue)
|
||||
} else {
|
||||
hideCategoryValues.splice(indx, 1)
|
||||
}
|
||||
state.hideCategoryValues = hideCategoryValues
|
||||
|
||||
if (state.hideCategoryValues.length === 0 && !colorBy) {
|
||||
state.appliedFilter = null
|
||||
updateState(state)
|
||||
viewer.applyFilter(state.appliedFilter)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.hideCategoryValues.length === 0 && colorBy) {
|
||||
state.appliedFilter = {
|
||||
colorBy: { type: 'category', property: filterKey }
|
||||
}
|
||||
}
|
||||
if (state.hideCategoryValues.length !== 0) {
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { not: state.hideCategoryValues } },
|
||||
colorBy: colorBy ? { type: 'category', property: filterKey } : null
|
||||
}
|
||||
}
|
||||
const res = await viewer.applyFilter(state.appliedFilter)
|
||||
state.colorLegend = res.colorLegend
|
||||
updateState(state)
|
||||
}
|
||||
|
||||
export async function toggleColorByCategory(params: { filterKey: string }) {
|
||||
const { filterKey } = params
|
||||
const state = { ...commitObjectViewerState() }
|
||||
const viewer = getInitializedViewer()
|
||||
|
||||
if (state.appliedFilter && state.appliedFilter.colorBy) {
|
||||
state.appliedFilter = {
|
||||
...state.appliedFilter,
|
||||
colorBy: null
|
||||
}
|
||||
} else {
|
||||
state.appliedFilter = {
|
||||
...state.appliedFilter,
|
||||
colorBy: { type: 'category', property: filterKey }
|
||||
}
|
||||
}
|
||||
const res = await viewer.applyFilter(state.appliedFilter)
|
||||
state.colorLegend = res.colorLegend
|
||||
updateState(state)
|
||||
}
|
||||
|
||||
function resetInternalCategoryObjectState() {
|
||||
updateState({
|
||||
isolateCategoryKey: null,
|
||||
isolateCategoryValues: [],
|
||||
hideCategoryKey: null,
|
||||
hideCategoryValues: []
|
||||
})
|
||||
}
|
||||
|
||||
export function setNumericFilter(params: {
|
||||
filterKey: string
|
||||
minValue: number
|
||||
maxValue: number
|
||||
gradientColors?: string[]
|
||||
}) {
|
||||
resetInternalHideIsolateObjectState()
|
||||
resetInternalCategoryObjectState()
|
||||
|
||||
const {
|
||||
filterKey,
|
||||
minValue,
|
||||
maxValue,
|
||||
gradientColors = ['#3F5EFB', '#FC466B']
|
||||
} = params
|
||||
const state = { ...commitObjectViewerState() }
|
||||
const viewer = getInitializedViewer()
|
||||
|
||||
state.appliedFilter = {
|
||||
ghostOthers: true,
|
||||
colorBy: {
|
||||
type: 'gradient',
|
||||
property: filterKey,
|
||||
minValue,
|
||||
maxValue,
|
||||
gradientColors
|
||||
},
|
||||
filterBy: { [filterKey]: { gte: minValue, lte: maxValue } }
|
||||
}
|
||||
|
||||
updateState(state)
|
||||
viewer.applyFilter(state.appliedFilter)
|
||||
}
|
||||
|
||||
// not sure if these filter types are 100% correct, I reverse engineered them
|
||||
// from the code
|
||||
type FilterByValue =
|
||||
| {
|
||||
gte: number
|
||||
lte: number
|
||||
}
|
||||
| string[]
|
||||
| { not: string[] }
|
||||
|
||||
export type Filter = {
|
||||
filterBy?: {
|
||||
__parents?: {
|
||||
includes?: string[]
|
||||
excludes?: string[]
|
||||
}
|
||||
} & {
|
||||
[by: string]: FilterByValue
|
||||
}
|
||||
colorBy?: {
|
||||
type: string
|
||||
property: string
|
||||
}
|
||||
ghostOthers?: boolean
|
||||
}
|
||||
|
||||
export async function setFilterDirectly(params: { filter: Filter }) {
|
||||
const { filter } = params
|
||||
|
||||
const isNotFilter = (filterByVal: FilterByValue): filterByVal is { not: string[] } =>
|
||||
has(filterByVal, 'not')
|
||||
|
||||
const filterBy = filter.filterBy
|
||||
if (filterBy && filterBy.__parents) {
|
||||
if (filterBy.__parents.includes) {
|
||||
isolateObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: filterBy.__parents.includes
|
||||
})
|
||||
return
|
||||
}
|
||||
if (filterBy.__parents.excludes) {
|
||||
hideObjects({
|
||||
filterKey: '__parents',
|
||||
filterValues: filterBy.__parents.excludes
|
||||
})
|
||||
return
|
||||
}
|
||||
} else if (filter.ghostOthers && filterBy) {
|
||||
// means it's isolate by category or numeric filter
|
||||
const filterByKey = Object.keys(filterBy || {})[0]
|
||||
const filterVal = filterBy[filterByKey]
|
||||
|
||||
if (
|
||||
filter.colorBy &&
|
||||
filter.colorBy.type === 'gradient' &&
|
||||
!isArray(filterVal) &&
|
||||
!isNotFilter(filterVal)
|
||||
) {
|
||||
setNumericFilter({
|
||||
filterKey: filterByKey,
|
||||
minValue: filterVal.gte,
|
||||
maxValue: filterVal.lte
|
||||
})
|
||||
} else if (isArray(filterVal)) {
|
||||
for (const val of filterVal) {
|
||||
const f = {
|
||||
filterKey: filterByKey,
|
||||
filterValue: val,
|
||||
allValues: [],
|
||||
colorBy: filter.colorBy
|
||||
}
|
||||
isolateCategoryToggle(f)
|
||||
}
|
||||
}
|
||||
} else if (filterBy) {
|
||||
const filterByKey = Object.keys(filterBy || {})[0]
|
||||
const filterVal = filterBy[filterByKey]
|
||||
|
||||
if (isNotFilter(filterVal)) {
|
||||
const values = filterVal.not
|
||||
for (const val of values) {
|
||||
const f = {
|
||||
filterKey: filterByKey,
|
||||
filterValue: val,
|
||||
allValues: [],
|
||||
colorBy: filter.colorBy
|
||||
}
|
||||
hideCategoryToggle(f)
|
||||
}
|
||||
}
|
||||
} else if (filter.colorBy) {
|
||||
toggleColorByCategory({ filterKey: filter.colorBy.property })
|
||||
}
|
||||
}
|
||||
|
||||
export function resetFilter() {
|
||||
const viewer = getInitializedViewer()
|
||||
|
||||
resetInternalHideIsolateObjectState()
|
||||
resetInternalCategoryObjectState()
|
||||
updateState({
|
||||
appliedFilter: null,
|
||||
preventCommentCollapse: true
|
||||
})
|
||||
|
||||
viewer.applyFilter(null)
|
||||
}
|
||||
|
||||
export function setPreventCommentCollapse(shouldPrevent: boolean) {
|
||||
updateState({
|
||||
preventCommentCollapse: shouldPrevent
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Viewer } from '@speckle/viewer'
|
||||
import { inject, InjectionKey, Ref, ref, provide } from 'vue'
|
||||
|
||||
// Keys you can use in the Viewer family of components to inject the viewer, its container and its init state
|
||||
export const ViewerKey: InjectionKey<Ref<Viewer>> = Symbol('VIEWER_INSTANCE')
|
||||
export const ViewerInitializedKey: InjectionKey<Ref<boolean>> = Symbol(
|
||||
'VIEWER_INSTANCE_INITIALIZED'
|
||||
)
|
||||
export const ViewerInitializedPromiseKey: InjectionKey<Promise<boolean>> = Symbol(
|
||||
'VIEWER_INSTANCE_INITIALIZED_PROMISE'
|
||||
)
|
||||
export const ViewerContainerKey: InjectionKey<Ref<HTMLElement>> =
|
||||
Symbol('VIEWER_CONTAINER')
|
||||
|
||||
/**
|
||||
* Inject viewer instance (it should be provided in an ancestor component using setupViewerInjection())
|
||||
*/
|
||||
export function useInjectedViewer() {
|
||||
// force casting, cause we know for a fact that these injections won't be undefined - handling the "or undefined" check everywhere
|
||||
// is going to be a pain in the ass
|
||||
const viewer = inject(ViewerKey) as Ref<Viewer>
|
||||
const container = inject(ViewerContainerKey) as Ref<HTMLElement>
|
||||
const isInitialized = inject(ViewerInitializedKey) as Ref<boolean>
|
||||
const isInitializedPromise = inject(ViewerInitializedPromiseKey) as Promise<boolean>
|
||||
|
||||
return { viewer, container, isInitialized, isInitializedPromise }
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass in a newly created Viewer instance and its container for injection down into child components
|
||||
* (through useInjectedViewer() or the injection keys manually).
|
||||
*/
|
||||
export function setupNewViewerInjection(params: {
|
||||
viewer: Viewer
|
||||
container: HTMLElement
|
||||
initPromise: Promise<void>
|
||||
}) {
|
||||
const viewer: Ref<Viewer> = ref(params.viewer) as Ref<Viewer>
|
||||
const container = ref<HTMLElement>(params.container)
|
||||
const isInitialized = ref(false)
|
||||
const isInitializedPromise = params.initPromise.then(
|
||||
() => (isInitialized.value = true)
|
||||
)
|
||||
|
||||
provide(ViewerKey, viewer)
|
||||
provide(ViewerContainerKey, container)
|
||||
provide(ViewerInitializedKey, isInitialized)
|
||||
provide(ViewerInitializedPromiseKey, isInitializedPromise)
|
||||
|
||||
return { viewer, container, isInitialized, isInitializedPromise }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Vector3 } from 'three'
|
||||
import { Viewer } from '@speckle/viewer'
|
||||
import CameraControls from 'camera-controls'
|
||||
|
||||
// we're relying on a private property here, which we shouldn't do
|
||||
// (I'm just migrating the function over from a previous file, I didn't write it)
|
||||
type RealCameraControls = CameraControls & { _zoom: number }
|
||||
|
||||
export function getCamArray(viewer: Viewer) {
|
||||
const controls = viewer.cameraHandler.activeCam.controls as RealCameraControls
|
||||
const pos = controls.getPosition(new Vector3())
|
||||
const target = controls.getTarget(new Vector3())
|
||||
const c = [
|
||||
parseFloat(pos.x.toFixed(5)),
|
||||
parseFloat(pos.y.toFixed(5)),
|
||||
parseFloat(pos.z.toFixed(5)),
|
||||
parseFloat(target.x.toFixed(5)),
|
||||
parseFloat(target.y.toFixed(5)),
|
||||
parseFloat(target.z.toFixed(5)),
|
||||
viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
|
||||
controls._zoom
|
||||
]
|
||||
return c
|
||||
}
|
||||
@@ -80,6 +80,7 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
import UserAvatar from '@/main/components/auth/UserAvatarAuthoriseApp'
|
||||
import UserAvatarIcon from '@/main/components/common/UserAvatarIcon'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
name: 'AuthorizeApp',
|
||||
@@ -124,7 +125,7 @@ export default {
|
||||
return `${this.app.redirectUrl}?denied=true`
|
||||
},
|
||||
selfUserId() {
|
||||
return localStorage.getItem('uuid')
|
||||
return AppLocalStorage.get('uuid')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -146,7 +147,7 @@ export default {
|
||||
window.location.replace(
|
||||
`${window.location.origin}/auth/accesscode?appId=${this.app.id}&challenge=${
|
||||
this.$route.params.challenge
|
||||
}&token=${localStorage.getItem('AuthToken')}&suuid=${this.$route.query.suuid}`
|
||||
}&token=${AppLocalStorage.get('AuthToken')}&suuid=${this.$route.query.suuid}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ import {
|
||||
getInviteTokenFromRoute,
|
||||
processSuccessfulAuth
|
||||
} from '@/main/lib/auth/services/authService'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
name: 'TheLogin',
|
||||
@@ -193,7 +194,7 @@ export default {
|
||||
|
||||
if (!challenge && this.appId === 'spklwebapp') {
|
||||
this.challenge = randomString(10)
|
||||
localStorage.setItem('appChallenge', this.challenge)
|
||||
AppLocalStorage.set('appChallenge', this.challenge)
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ import {
|
||||
getInviteTokenFromRoute,
|
||||
processSuccessfulAuth
|
||||
} from '@/main/lib/auth/services/authService'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
name: 'TheRegistration',
|
||||
@@ -289,7 +290,7 @@ export default {
|
||||
|
||||
if (!challenge && this.appId === 'spklwebapp') {
|
||||
this.challenge = randomString({ length: 10 })
|
||||
localStorage.setItem('appChallenge', this.challenge)
|
||||
AppLocalStorage.set('appChallenge', this.challenge)
|
||||
} else if (challenge) {
|
||||
this.challenge = challenge
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="commit-object-viewer">
|
||||
<div v-if="(isMultiple || isCommit || isObject) && !singleResourceError">
|
||||
<commit-toolbar
|
||||
v-if="isCommit"
|
||||
@@ -9,77 +9,86 @@
|
||||
<object-toolbar v-if="isObject" :stream="resources[0].data" />
|
||||
<multiple-resources-toolbar
|
||||
v-if="isMultiple"
|
||||
:stream="{ name: resources[0].data.name, id: $route.params.streamId }"
|
||||
:stream="{ name: resources[0].data.name, id: streamId }"
|
||||
:resources="resources"
|
||||
/>
|
||||
|
||||
<portal v-if="canRenderNavPortal" to="nav">
|
||||
<div v-if="!$loggedIn()" class="px-4 my-2">
|
||||
<v-btn small block color="primary" @click="$loginAndSetRedirect()">
|
||||
Sign In
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-list nav dense class="mt-0 pt-0">
|
||||
<v-list-item
|
||||
v-if="isCommit"
|
||||
link
|
||||
:to="`/streams/${$route.params.streamId}/branches/${resources[0].data.commit.branchName}`"
|
||||
class=""
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
<v-icon small class="mr-1 caption">mdi-source-branch</v-icon>
|
||||
{{ resources[0].data.commit.branchName }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="isObject || isMultiple"
|
||||
link
|
||||
exact
|
||||
:to="`/streams/${$route.params.streamId}`"
|
||||
class=""
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
<v-icon small class="mr-1 caption">mdi-home</v-icon>
|
||||
Stream Home
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<prioritized-portal to="nav" identity="stream-commit-viewer" :priority="2">
|
||||
<commit-object-viewer-scope
|
||||
:stream-id="streamId"
|
||||
:resource-id="resourceId"
|
||||
:is-embed="isEmbed"
|
||||
>
|
||||
<template v-if="!isEmbed">
|
||||
<div v-if="!$loggedIn()" class="px-4 my-2">
|
||||
<v-btn small block color="primary" @click="$loginAndSetRedirect()">
|
||||
Sign In
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-list nav dense class="mt-0 pt-0">
|
||||
<v-list-item
|
||||
v-if="isCommit"
|
||||
link
|
||||
:to="`/streams/${streamId}/branches/${resources[0].data.commit.branchName}`"
|
||||
class=""
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
<v-icon small class="mr-1 caption">mdi-source-branch</v-icon>
|
||||
{{ resources[0].data.commit.branchName }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="isObject || isMultiple"
|
||||
link
|
||||
exact
|
||||
:to="`/streams/${streamId}`"
|
||||
class=""
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon small class>mdi-arrow-left-drop-circle</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
<v-icon small class="mr-1 caption">mdi-home</v-icon>
|
||||
Stream Home
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<!-- Loaded resources -->
|
||||
<resource-group
|
||||
:resources="resources"
|
||||
@remove="removeResource"
|
||||
@add-resource="addResource"
|
||||
@show-add-overlay="showAddOverlay = true"
|
||||
/>
|
||||
<!-- Loaded resources -->
|
||||
<resource-group
|
||||
:resources="resources"
|
||||
:allow-add="!isEmbed"
|
||||
@remove="removeResource"
|
||||
@add-resource="addResource"
|
||||
@show-add-overlay="showAddOverlay = true"
|
||||
/>
|
||||
|
||||
<!-- <v-divider v-if="isMultiple" class="my-4" /> -->
|
||||
<portal-target name="comments"></portal-target>
|
||||
<!-- Views display -->
|
||||
<views-display v-if="views.length !== 0" :views="views" class="mt-4" />
|
||||
<!-- <v-divider v-if="isMultiple" class="my-4" /> -->
|
||||
<portal-target name="comments"></portal-target>
|
||||
<!-- Views display -->
|
||||
<views-display v-if="views.length !== 0" :views="views" class="mt-4" />
|
||||
|
||||
<!-- Filters display -->
|
||||
<viewer-filters
|
||||
class="mt-4"
|
||||
:props="objectProperties"
|
||||
:source-application="
|
||||
resources
|
||||
.filter((r) => r.type === 'commit')
|
||||
.map((r) => r.data.commit.sourceApplication)
|
||||
.join(',')
|
||||
"
|
||||
/>
|
||||
</portal>
|
||||
<!-- Filters display -->
|
||||
<viewer-filters
|
||||
class="mt-4"
|
||||
:props="objectProperties"
|
||||
:source-application="
|
||||
resources
|
||||
.filter((r) => r.type === 'commit')
|
||||
.map((r) => r.data.commit.sourceApplication)
|
||||
.join(',')
|
||||
"
|
||||
/>
|
||||
</commit-object-viewer-scope>
|
||||
</prioritized-portal>
|
||||
|
||||
<!-- Preview image -->
|
||||
<v-fade-transition>
|
||||
@@ -88,14 +97,14 @@
|
||||
:style="`
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
${!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'}
|
||||
${topOffsetStyle}
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
opacity: 0.7;
|
||||
filter: blur(4px);
|
||||
`"
|
||||
:height="420"
|
||||
:url="`/preview/${$route.params.streamId}/objects/${
|
||||
:url="`/preview/${streamId}/objects/${
|
||||
isCommit
|
||||
? resources[0].data.commit.referencedObject
|
||||
: resources[0].data.object.id
|
||||
@@ -104,9 +113,7 @@
|
||||
</v-fade-transition>
|
||||
|
||||
<div
|
||||
:style="`height: 100vh; width: 100%; ${
|
||||
!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'
|
||||
} left: 0px; position: absolute`"
|
||||
:style="`height: 100vh; width: 100%; ${topOffsetStyle} left: 0px; position: absolute`"
|
||||
>
|
||||
<speckle-viewer @load-progress="captureProgress" @selection="captureSelect" />
|
||||
</div>
|
||||
@@ -115,7 +122,7 @@
|
||||
:style="`
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
${!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'}
|
||||
${topOffsetStyle}
|
||||
left: 22px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
@@ -125,7 +132,7 @@
|
||||
v-show="selectionData.length !== 0"
|
||||
:key="'one'"
|
||||
:objects="selectionData"
|
||||
:stream-id="$route.params.streamId"
|
||||
:stream-id="streamId"
|
||||
@clear-selection="selectionData = []"
|
||||
/>
|
||||
</div>
|
||||
@@ -143,7 +150,7 @@
|
||||
:style="`
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
${!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'}
|
||||
${topOffsetStyle}
|
||||
left: 0;
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
@@ -154,7 +161,7 @@
|
||||
>
|
||||
<viewer-bubbles key="a" />
|
||||
<comments-overlay key="c" @add-resources="addResources" />
|
||||
<comment-add-overlay key="b" />
|
||||
<comment-add-overlay v-if="!isEmbed" key="b" />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@@ -198,7 +205,7 @@
|
||||
<div v-else-if="singleResourceError">
|
||||
<error-placeholder error-type="404">
|
||||
<h2>
|
||||
<code>{{ $route.params.resourceId }}</code>
|
||||
<code>{{ resourceId }}</code>
|
||||
not found.
|
||||
</h2>
|
||||
</error-placeholder>
|
||||
@@ -210,7 +217,7 @@
|
||||
style="z-index: 10000"
|
||||
>
|
||||
<stream-overlay-viewer
|
||||
:stream-id="$route.params.streamId"
|
||||
:stream-id="streamId"
|
||||
@add-resource="addResource"
|
||||
@close="showAddOverlay = false"
|
||||
/>
|
||||
@@ -225,93 +232,172 @@
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, toRefs } from 'vue'
|
||||
import debounce from 'lodash/debounce'
|
||||
import streamCommitQuery from '@/graphql/commit.gql'
|
||||
import streamObjectQuery from '@/graphql/objectSingleNoData.gql'
|
||||
import SpeckleViewer from '@/main/components/common/SpeckleViewer.vue' // do not import async
|
||||
import { resourceType } from '@/plugins/resourceIdentifier'
|
||||
import {
|
||||
STANDARD_PORTAL_KEYS,
|
||||
buildPortalStateMixin
|
||||
} from '@/main/utils/portalStateManager'
|
||||
Filter,
|
||||
setFilterDirectly,
|
||||
setIsViewerBusy,
|
||||
setupCommitObjectViewer
|
||||
} from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import gql from 'graphql-tag'
|
||||
import {
|
||||
StreamCommitQueryQuery,
|
||||
StreamObjectNoDataQuery
|
||||
} from '@/graphql/generated/graphql'
|
||||
import { Get } from 'type-fest'
|
||||
import { has } from 'lodash'
|
||||
import { Nullable } from '@/helpers/typeHelpers'
|
||||
import { getCamArray } from '@/main/lib/viewer/core/helpers/cameraHelper'
|
||||
import CommitObjectViewerScope from '@/main/components/viewer/CommitObjectViewerScope.vue'
|
||||
import PrioritizedPortal from '@/main/components/common/utility/PrioritizedPortal.vue'
|
||||
|
||||
export default {
|
||||
type ErroredResourceData = {
|
||||
error: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
type CommitResourceData = NonNullable<Get<StreamCommitQueryQuery, 'stream'>>
|
||||
|
||||
type ObjectResourceData = NonNullable<Get<StreamObjectNoDataQuery, 'stream'>>
|
||||
|
||||
type AllSupportedDataTypes =
|
||||
| ErroredResourceData
|
||||
| CommitResourceData
|
||||
| ObjectResourceData
|
||||
|
||||
type ResourceTypeValue = 'commit' | 'object'
|
||||
|
||||
type ResourceObjectType<T> = {
|
||||
type: ResourceTypeValue
|
||||
id: string
|
||||
data: T
|
||||
}
|
||||
|
||||
const isErrorResource = (
|
||||
resource: ResourceObjectType<unknown>
|
||||
): resource is ResourceObjectType<ErroredResourceData> => has(resource.data, 'error')
|
||||
|
||||
const isCommitResource = (
|
||||
resource: ResourceObjectType<unknown>
|
||||
): resource is ResourceObjectType<CommitResourceData> => resource.type === 'commit'
|
||||
|
||||
const isObjectResource = (
|
||||
resource: ResourceObjectType<unknown>
|
||||
): resource is ResourceObjectType<ObjectResourceData> => resource.type === 'object'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CommitObjectViewer',
|
||||
components: {
|
||||
SpeckleViewer,
|
||||
CommitToolbar: () => import('@/main/toolbars/CommitToolbar'),
|
||||
ObjectToolbar: () => import('@/main/toolbars/ObjectToolbar'),
|
||||
MultipleResourcesToolbar: () => import('@/main/toolbars/MultipleResourcesToolbar'),
|
||||
CommitEdit: () => import('@/main/dialogs/CommitEdit'),
|
||||
CommitObjectViewerScope,
|
||||
PrioritizedPortal,
|
||||
CommitToolbar: () => import('@/main/toolbars/CommitToolbar.vue'),
|
||||
ObjectToolbar: () => import('@/main/toolbars/ObjectToolbar.vue'),
|
||||
MultipleResourcesToolbar: () =>
|
||||
import('@/main/toolbars/MultipleResourcesToolbar.vue'),
|
||||
CommitEdit: () => import('@/main/dialogs/CommitEdit.vue'),
|
||||
StreamOverlayViewer: () =>
|
||||
import('@/main/components/viewer/dialogs/StreamOverlayViewer.vue'),
|
||||
ErrorPlaceholder: () => import('@/main/components/common/ErrorPlaceholder'),
|
||||
PreviewImage: () => import('@/main/components/common/PreviewImage'),
|
||||
ViewerControls: () => import('@/main/components/viewer/ViewerControls'),
|
||||
ObjectSelection: () => import('@/main/components/viewer/ObjectSelection'),
|
||||
ResourceGroup: () => import('@/main/components/viewer/ResourceGroup'),
|
||||
ViewsDisplay: () => import('@/main/components/viewer/ViewsDisplay'),
|
||||
ErrorPlaceholder: () => import('@/main/components/common/ErrorPlaceholder.vue'),
|
||||
PreviewImage: () => import('@/main/components/common/PreviewImage.vue'),
|
||||
ViewerControls: () => import('@/main/components/viewer/ViewerControls.vue'),
|
||||
ObjectSelection: () => import('@/main/components/viewer/ObjectSelection.vue'),
|
||||
ResourceGroup: () => import('@/main/components/viewer/ResourceGroup.vue'),
|
||||
ViewsDisplay: () => import('@/main/components/viewer/ViewsDisplay.vue'),
|
||||
ViewerFilters: () => import('@/main/components/viewer/ViewerFilters.vue'),
|
||||
ViewerBubbles: () => import('@/main/components/viewer/ViewerBubbles.vue'),
|
||||
CommentAddOverlay: () => import('@/main/components/viewer/CommentAddOverlay'),
|
||||
CommentsOverlay: () => import('@/main/components/viewer/CommentsOverlay')
|
||||
CommentAddOverlay: () => import('@/main/components/viewer/CommentAddOverlay.vue'),
|
||||
CommentsOverlay: () => import('@/main/components/viewer/CommentsOverlay.vue')
|
||||
},
|
||||
props: {
|
||||
streamId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* Commit or Object ID
|
||||
*/
|
||||
resourceId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isEmbed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const { viewer } = setupCommitObjectViewer(toRefs(props))
|
||||
const { result: viewerStateResult } = useQuery(gql`
|
||||
query {
|
||||
commitObjectViewerState @client {
|
||||
appliedFilter
|
||||
}
|
||||
}
|
||||
`)
|
||||
const viewerState = computed(
|
||||
() => viewerStateResult.value?.commitObjectViewerState || {}
|
||||
)
|
||||
|
||||
return {
|
||||
viewer,
|
||||
viewerState
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
buildPortalStateMixin([STANDARD_PORTAL_KEYS.Nav], 'stream-commit-viewer', 1)
|
||||
],
|
||||
data: () => ({
|
||||
firstCallToCam: false,
|
||||
camToSet: null as Nullable<number[]>,
|
||||
filterToSet: null as Nullable<Filter>,
|
||||
loadedModel: false,
|
||||
loadProgress: 0,
|
||||
showCommitEditDialog: false,
|
||||
selectionData: [],
|
||||
views: [],
|
||||
objectProperties: null,
|
||||
hiddenObjects: [],
|
||||
isolatedObjects: [],
|
||||
showVisReset: false,
|
||||
resourceType: null,
|
||||
resources: [],
|
||||
selectionData: [] as Record<string, unknown>[],
|
||||
views: [] as Record<string, unknown>[],
|
||||
objectProperties: null as Nullable<Record<string, unknown>>,
|
||||
resources: [] as ResourceObjectType<AllSupportedDataTypes>[],
|
||||
showAddOverlay: false,
|
||||
viewerBusy: false
|
||||
}),
|
||||
computed: {
|
||||
isCommit() {
|
||||
topOffsetStyle(): string {
|
||||
if (this.isEmbed) return 'top: 0;'
|
||||
return !this.$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'
|
||||
},
|
||||
isCommit(): boolean {
|
||||
if (this.resources.length === 0) return false
|
||||
if (this.resources.length === 1 && this.resources[0].type === 'commit')
|
||||
return true
|
||||
return false
|
||||
},
|
||||
isObject() {
|
||||
isObject(): boolean {
|
||||
if (this.resources.length === 0) return false
|
||||
if (this.resources.length === 1 && this.resources[0].type === 'object')
|
||||
return true
|
||||
return false
|
||||
},
|
||||
isMultiple() {
|
||||
isMultiple(): boolean {
|
||||
if (this.resources.length === 0) return false
|
||||
if (this.resources.length > 1) return true
|
||||
return false
|
||||
},
|
||||
singleResourceError() {
|
||||
return this.resources.length === 1 && this.resources[0].data.error
|
||||
singleResourceError(): boolean {
|
||||
if (this.resources.length !== 1) return false
|
||||
const resource = this.resources[0]
|
||||
if (!isErrorResource(resource)) return false
|
||||
return resource.data.error
|
||||
},
|
||||
overlay(): Nullable<string> {
|
||||
return this.$route.query.overlay ? (this.$route.query.overlay as string) : null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
stream(val) {
|
||||
if (!val) return
|
||||
if (
|
||||
val &&
|
||||
val.commit &&
|
||||
val.commit.branchName &&
|
||||
val.commit.branchName === 'globals'
|
||||
) {
|
||||
this.$router.push(
|
||||
`/streams/${this.$route.params.streamId}/globals/${val.commit.id}`
|
||||
)
|
||||
return
|
||||
}
|
||||
},
|
||||
'$store.state.appliedFilter'(val) {
|
||||
'viewerState.appliedFilter'(val) {
|
||||
if (!val) {
|
||||
const fullQuery = { ...this.$route.query }
|
||||
delete fullQuery.filter
|
||||
@@ -334,20 +420,20 @@ export default {
|
||||
async mounted() {
|
||||
this.$eventHub.$emit('page-load', true)
|
||||
this.resources.push({
|
||||
type: resourceType(this.$route.params.resourceId),
|
||||
id: this.$route.params.resourceId,
|
||||
type: this.resolveResourceType(this.resourceId),
|
||||
id: this.resourceId,
|
||||
data:
|
||||
resourceType(this.$route.params.resourceId) === 'commit'
|
||||
? await this.loadCommit(this.$route.params.resourceId)
|
||||
: await this.loadObject(this.$route.params.resourceId)
|
||||
this.resolveResourceType(this.resourceId) === 'commit'
|
||||
? await this.loadCommit(this.resourceId)
|
||||
: await this.loadObject(this.resourceId)
|
||||
})
|
||||
|
||||
if (this.$route.query.overlay) {
|
||||
const ids = this.$route.query.overlay.split(',')
|
||||
if (this.overlay) {
|
||||
const ids = this.overlay.split(',')
|
||||
for (const id of ids) {
|
||||
const cleanedId = id.replace(/\s+/g, '')
|
||||
if (!cleanedId || cleanedId === '') continue
|
||||
const resType = resourceType(cleanedId)
|
||||
const resType = this.resolveResourceType(cleanedId)
|
||||
this.resources.push({
|
||||
type: resType,
|
||||
id: cleanedId,
|
||||
@@ -359,13 +445,15 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// If global variables commit, redirect to globals editor page
|
||||
if (
|
||||
!this.isEmbed &&
|
||||
this.resources.length === 1 &&
|
||||
this.resources[0].type === 'commit' &&
|
||||
this.resources[0].data.commit.branchName === 'globals'
|
||||
isCommitResource(this.resources[0]) &&
|
||||
this.resources[0].data.commit?.branchName === 'globals'
|
||||
) {
|
||||
this.$router.push(
|
||||
`/streams/${this.$route.params.streamId}/globals/${this.resources[0].data.commit.id}`
|
||||
`/streams/${this.streamId}/globals/${this.resources[0].data.commit.id}`
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -375,40 +463,46 @@ export default {
|
||||
this.camToSet = null
|
||||
this.filterToSet = null
|
||||
|
||||
if (this.$route.query && this.$route.query.c) {
|
||||
this.camToSet = JSON.parse(this.$route.query.c)
|
||||
if (this.$route.query?.c) {
|
||||
this.camToSet = JSON.parse(this.$route.query.c as string)
|
||||
}
|
||||
|
||||
if (this.$route.query && this.$route.query.filter) {
|
||||
this.filterToSet = JSON.parse(this.$route.query.filter)
|
||||
if (this.$route.query?.filter) {
|
||||
this.filterToSet = JSON.parse(this.$route.query.filter as string)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
for (const resource of this.resources) {
|
||||
if (resource.data.error) continue
|
||||
this.loadModel(
|
||||
resource.type === 'commit'
|
||||
? resource.data.commit.referencedObject
|
||||
: resource.data.object.id
|
||||
)
|
||||
if (isErrorResource(resource)) continue
|
||||
|
||||
let modelId: string | undefined = undefined
|
||||
if (isCommitResource(resource)) {
|
||||
modelId = resource.data.commit?.referencedObject
|
||||
} else if (isObjectResource(resource)) {
|
||||
modelId = resource.data.object?.id
|
||||
}
|
||||
|
||||
if (modelId) {
|
||||
this.loadModel(modelId)
|
||||
}
|
||||
}
|
||||
window.__viewer.on('busy', (val) => {
|
||||
this.$store.commit('setViewerBusy', { viewerBusyState: val })
|
||||
|
||||
this.viewer.on('busy', (val: boolean) => {
|
||||
setIsViewerBusy(!!val)
|
||||
this.viewerBusy = val
|
||||
if (!val && this.camToSet) {
|
||||
setTimeout(() => {
|
||||
if (!this.camToSet) return
|
||||
|
||||
if (this.camToSet[6] === 1) {
|
||||
window.__viewer.toggleCameraProjection()
|
||||
this.viewer.toggleCameraProjection()
|
||||
}
|
||||
window.__viewer.interactions.setLookAt(
|
||||
this.viewer.interactions.setLookAt(
|
||||
{ x: this.camToSet[0], y: this.camToSet[1], z: this.camToSet[2] }, // position
|
||||
{ x: this.camToSet[3], y: this.camToSet[4], z: this.camToSet[5] } // target
|
||||
)
|
||||
if (this.camToSet[6] === 1) {
|
||||
window.__viewer.cameraHandler.activeCam.controls.zoom(
|
||||
this.camToSet[7],
|
||||
true
|
||||
)
|
||||
this.viewer.cameraHandler.activeCam.controls.zoom(this.camToSet[7], true)
|
||||
}
|
||||
this.camToSet = null
|
||||
}, 200)
|
||||
@@ -416,13 +510,15 @@ export default {
|
||||
|
||||
if (!val && this.filterToSet) {
|
||||
setTimeout(() => {
|
||||
this.$store.commit('setFilterDirect', { filter: this.filterToSet })
|
||||
if (!this.filterToSet) return
|
||||
|
||||
setFilterDirectly({ filter: this.filterToSet })
|
||||
this.filterToSet = null
|
||||
}, 200)
|
||||
}
|
||||
})
|
||||
|
||||
window.__viewer.cameraHandler.controls.addEventListener(
|
||||
this.viewer.cameraHandler.controls.addEventListener(
|
||||
'rest',
|
||||
debounce(() => {
|
||||
if (!(this.$route.name === 'commit' || this.$route.name === 'object')) {
|
||||
@@ -434,19 +530,7 @@ export default {
|
||||
}
|
||||
if (this.camToSet) return
|
||||
|
||||
const controls = window.__viewer.cameraHandler.activeCam.controls
|
||||
const pos = controls.getPosition()
|
||||
const target = controls.getTarget()
|
||||
const c = [
|
||||
parseFloat(pos.x.toFixed(5)),
|
||||
parseFloat(pos.y.toFixed(5)),
|
||||
parseFloat(pos.z.toFixed(5)),
|
||||
parseFloat(target.x.toFixed(5)),
|
||||
parseFloat(target.y.toFixed(5)),
|
||||
parseFloat(target.z.toFixed(5)),
|
||||
window.__viewer.cameraHandler.activeCam.name === 'ortho' ? 1 : 0,
|
||||
controls._zoom
|
||||
]
|
||||
const c = getCamArray(this.viewer)
|
||||
const fullQuery = { ...this.$route.query }
|
||||
delete fullQuery.c
|
||||
this.$router
|
||||
@@ -457,14 +541,19 @@ export default {
|
||||
.catch(() => {})
|
||||
}, 1000)
|
||||
)
|
||||
|
||||
this.$emit('models-loaded')
|
||||
}, 300)
|
||||
},
|
||||
methods: {
|
||||
async loadCommit(id) {
|
||||
resolveResourceType(resourceId: string): ResourceTypeValue {
|
||||
return resourceId.length === 10 ? 'commit' : 'object'
|
||||
},
|
||||
async loadCommit(id: string) {
|
||||
try {
|
||||
const res = await this.$apollo.query({
|
||||
query: streamCommitQuery,
|
||||
variables: { streamId: this.$route.params.streamId, id }
|
||||
variables: { streamId: this.streamId, id }
|
||||
})
|
||||
if (res.data.stream.commit === null) throw new Error()
|
||||
return res.data.stream
|
||||
@@ -473,11 +562,11 @@ export default {
|
||||
return { error: true, message: `Failed to load commit ${id}` }
|
||||
}
|
||||
},
|
||||
async loadObject(id) {
|
||||
async loadObject(id: string) {
|
||||
try {
|
||||
const res = await this.$apollo.query({
|
||||
query: streamObjectQuery,
|
||||
variables: { streamId: this.$route.params.streamId, id }
|
||||
variables: { streamId: this.streamId, id }
|
||||
})
|
||||
if (res.data.stream.object === null) throw new Error()
|
||||
return res.data.stream
|
||||
@@ -486,28 +575,24 @@ export default {
|
||||
return { error: true, message: `Failed to load object ${id}` }
|
||||
}
|
||||
},
|
||||
async loadModel(objectId) {
|
||||
if (!window.__viewer) {
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: 'Error in rendering page (no __viewer found). Please refresh.'
|
||||
})
|
||||
}
|
||||
await window.__viewer.loadObject(
|
||||
`${window.location.origin}/streams/${this.$route.params.streamId}/objects/${objectId}`
|
||||
async loadModel(objectId: string) {
|
||||
await this.viewer.loadObject(
|
||||
`${window.location.origin}/streams/${this.streamId}/objects/${objectId}`
|
||||
)
|
||||
window.__viewer.zoomExtents(undefined, true)
|
||||
this.viewer.zoomExtents(undefined, true)
|
||||
|
||||
this.loadedModel = true
|
||||
this.setFilters()
|
||||
this.setViews()
|
||||
},
|
||||
async addResources(ids) {
|
||||
async addResources(ids: string[]) {
|
||||
for (const id of ids) {
|
||||
await this.addResource(id)
|
||||
}
|
||||
},
|
||||
async addResource(resId) {
|
||||
async addResource(resId: string) {
|
||||
this.showAddOverlay = false
|
||||
const resType = resourceType(resId)
|
||||
const resType = this.resolveResourceType(resId)
|
||||
const existing = this.resources.findIndex((res) => res.id === resId)
|
||||
|
||||
if (existing !== -1) {
|
||||
@@ -535,8 +620,8 @@ export default {
|
||||
// TODO add to url
|
||||
const fullQuery = { ...this.$route.query }
|
||||
delete fullQuery.overlay
|
||||
if (this.$route.query.overlay) {
|
||||
const arr = this.$route.query.overlay
|
||||
if (this.overlay) {
|
||||
const arr = this.overlay
|
||||
.split(',')
|
||||
.map((id) => id.replace(/\s+/g, ''))
|
||||
.filter((id) => id && id !== '' && id !== resource.id)
|
||||
@@ -558,16 +643,18 @@ export default {
|
||||
: resource.data.object.id
|
||||
)
|
||||
},
|
||||
async removeResource(resource) {
|
||||
async removeResource(resource: ResourceObjectType<AllSupportedDataTypes>) {
|
||||
const index = this.resources.findIndex((res) => resource.id === res.id)
|
||||
|
||||
if (index === -1) return // err
|
||||
|
||||
if (!resource.data.error) {
|
||||
if (
|
||||
!isErrorResource(resource) &&
|
||||
(isCommitResource(resource) || isObjectResource(resource))
|
||||
) {
|
||||
const url = `${window.location.origin}/streams/${resource.data.id}/objects/${
|
||||
resource.type === 'commit'
|
||||
? resource.data.commit.referencedObject
|
||||
: resource.data.object.id
|
||||
isCommitResource(resource)
|
||||
? resource.data.commit?.referencedObject
|
||||
: resource.data.object?.id
|
||||
}`
|
||||
|
||||
this.$mixpanel.track('Viewer Action', {
|
||||
@@ -576,14 +663,14 @@ export default {
|
||||
resourceType: resource.type
|
||||
})
|
||||
|
||||
await window.__viewer.unloadObject(url)
|
||||
window.__viewer.zoomExtents(undefined, true)
|
||||
await this.viewer.unloadObject(url)
|
||||
this.viewer.zoomExtents(undefined, true)
|
||||
}
|
||||
this.resources.splice(index, 1)
|
||||
this.setFilters()
|
||||
this.setViews()
|
||||
if (this.$route.query.overlay) {
|
||||
const arr = this.$route.query.overlay
|
||||
if (this.overlay) {
|
||||
const arr = this.overlay
|
||||
.split(',')
|
||||
.map((id) => id.replace(/\s+/g, ''))
|
||||
.filter((id) => id && id !== '' && id !== resource.id)
|
||||
@@ -604,25 +691,25 @@ export default {
|
||||
},
|
||||
setViews() {
|
||||
this.views.splice(0, this.views.length)
|
||||
this.views.push(...window.__viewer.sceneManager.views)
|
||||
this.views.push(...this.viewer.sceneManager.views)
|
||||
},
|
||||
async setFilters() {
|
||||
try {
|
||||
// repopulate object props
|
||||
this.objectProperties = await window.__viewer.getObjectsProperties()
|
||||
this.objectProperties = await this.viewer.getObjectsProperties()
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit('notification', {
|
||||
text: 'Failed to get object properties from viewer.'
|
||||
})
|
||||
}
|
||||
},
|
||||
captureProgress(args) {
|
||||
captureProgress(args: { progress: number }) {
|
||||
this.loadProgress = args.progress * 100
|
||||
},
|
||||
captureSelect(selectionData) {
|
||||
captureSelect(selectionData: { userData: Record<string, unknown>[] }) {
|
||||
this.selectionData.splice(0, this.selectionData.length)
|
||||
this.selectionData.push(...selectionData.userData)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -107,6 +107,7 @@ import {
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from '@/main/lib/core/composables/router'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
name: 'TheBranch',
|
||||
@@ -207,7 +208,7 @@ export default {
|
||||
return this.$apollo.loading || this.streamLoading
|
||||
},
|
||||
loggedInUserId() {
|
||||
return localStorage.getItem('uuid')
|
||||
return AppLocalStorage.get('uuid')
|
||||
},
|
||||
streamId() {
|
||||
return this.$route.params.streamId
|
||||
|
||||
@@ -145,6 +145,7 @@ import LeaveStreamPanel from '@/main/components/stream/collaborators/LeaveStream
|
||||
import { IsLoggedInMixin } from '@/main/lib/core/mixins/isLoggedInMixin'
|
||||
import { vueWithMixins } from '@/helpers/typeHelpers'
|
||||
import { convertThrowIntoFetchResult } from '@/main/lib/common/apollo/helpers/apolloOperationHelper'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default vueWithMixins(IsLoggedInMixin).extend({
|
||||
// @vue/component
|
||||
@@ -265,7 +266,7 @@ export default vueWithMixins(IsLoggedInMixin).extend({
|
||||
return users
|
||||
},
|
||||
myId() {
|
||||
return localStorage.getItem('uuid')
|
||||
return AppLocalStorage.get('uuid')
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
v-if="c"
|
||||
:comment="c"
|
||||
:stream="stream"
|
||||
:stream-id="$route.params.streamId"
|
||||
@deleted="handleDeletion"
|
||||
/>
|
||||
</v-col>
|
||||
@@ -96,6 +97,7 @@ import {
|
||||
buildPortalStateMixin
|
||||
} from '@/main/utils/portalStateManager'
|
||||
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
|
||||
import { setSelectedCommentMetaData } from '@/main/lib/viewer/commit-object-viewer/stateManager'
|
||||
|
||||
export default {
|
||||
name: 'TheComments',
|
||||
@@ -193,7 +195,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleDeletion(comment) {
|
||||
this.$store.commit('setCommentSelection', { comment: null })
|
||||
setSelectedCommentMetaData(null)
|
||||
const indx = this.localComments.findIndex((lc) => lc.id === comment.id)
|
||||
this.localComments.splice(indx, 1)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
<template>
|
||||
<v-app
|
||||
:class="`embed-viewer no-scrollbar ${
|
||||
transparent ? '' : $vuetify.theme.dark ? 'background-dark' : 'background-light'
|
||||
}`"
|
||||
>
|
||||
<!-- BG image -->
|
||||
<div
|
||||
v-if="resourceMetadata && !isModelLoaded"
|
||||
style="position: fixed; top: 0; width: 100%; height: 100%; cursor: pointer"
|
||||
class="embed-bg"
|
||||
@click="load()"
|
||||
>
|
||||
<preview-image :url="previewUrl" :height="height" rotate></preview-image>
|
||||
</div>
|
||||
|
||||
<!-- Play button -->
|
||||
<div
|
||||
v-if="!isModelLoaded && !error"
|
||||
class="viewer-play d-flex fullscreen align-center justify-center no-mouse"
|
||||
>
|
||||
<v-btn
|
||||
id="viewer-play-btn"
|
||||
:disabled="showPlayLoader"
|
||||
fab
|
||||
color="primary"
|
||||
class="elevation-4 hover-tada mouse"
|
||||
@click="load()"
|
||||
>
|
||||
<v-icon v-if="!showPlayLoader">mdi-play</v-icon>
|
||||
<v-icon v-else class="spinning-icon">mdi-loading</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- This should always be conditionally and asynchronously loaded so that heavy viewer deps are lazy loaded -->
|
||||
<embedded-commit-object-viewer
|
||||
v-if="shouldLoadHeavyDeps"
|
||||
:stream-id="streamId"
|
||||
:resource-id="resourceMetadata.resourceId"
|
||||
@models-loaded="onModelsLoaded"
|
||||
/>
|
||||
|
||||
<!-- Display error if needed -->
|
||||
<div v-if="error" class="fullscreen d-flex justify-center align-center">
|
||||
<div class="">
|
||||
<p class="text-h5 text-center red--text">Speckle Embedding Error</p>
|
||||
<p class="text-center grey--text">
|
||||
Double check to see if the stream is public and if the embed link is correct.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Nullable } from '@/helpers/typeHelpers'
|
||||
import { computed, defineComponent, ref, onBeforeMount, watch, onMounted } from 'vue'
|
||||
import { useRoute } from '@/main/lib/core/composables/router'
|
||||
import { useMixpanel } from '@/main/lib/core/composables/tracking'
|
||||
import { useApolloClient } from '@vue/apollo-composable'
|
||||
import {
|
||||
StreamBranchFirstCommitDocument,
|
||||
StreamFirstCommitDocument
|
||||
} from '@/graphql/generated/graphql'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { isDarkTheme } from '@/main/utils/themeStateManager'
|
||||
import PreviewImage from '@/main/components/common/PreviewImage.vue'
|
||||
import { useEmbedViewerQuery } from '@/main/lib/viewer/commit-object-viewer/composables/embed'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TheEmbed',
|
||||
components: {
|
||||
EmbeddedCommitObjectViewer: () =>
|
||||
import('@/main/components/viewer/embed/EmbeddedCommitObjectViewer.vue'),
|
||||
PreviewImage
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const apollo = useApolloClient()
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const isInitialized = ref(false)
|
||||
const isModelLoaded = ref(false)
|
||||
const shouldLoadHeavyDeps = ref(false)
|
||||
|
||||
const error = ref(null as Nullable<Error>)
|
||||
const resourceMetadata = ref(
|
||||
null as Nullable<{
|
||||
resourceId: string
|
||||
objectId?: Nullable<string>
|
||||
type: 'commit' | 'object'
|
||||
}>
|
||||
)
|
||||
|
||||
const displayType = computed(() => {
|
||||
let type: string | null = null
|
||||
if (route.query.stream) type = 'stream'
|
||||
if (route.query.branch) type = 'branch'
|
||||
if (route.query.commit) type = 'commit'
|
||||
if (route.query.object) type = 'object'
|
||||
|
||||
return type
|
||||
})
|
||||
|
||||
const { height } = useWindowSize()
|
||||
|
||||
const { streamId, branchName, commitId, objectId, transparent } =
|
||||
useEmbedViewerQuery()
|
||||
|
||||
const previewUrl = computed(() => {
|
||||
if (!resourceMetadata.value || !streamId.value) return null
|
||||
|
||||
if (resourceMetadata.value.objectId) {
|
||||
return `/preview/${streamId.value}/objects/${resourceMetadata.value.objectId}`
|
||||
} else if (resourceMetadata.value.type === 'commit') {
|
||||
return `/preview/${streamId.value}/commits/${resourceMetadata.value.resourceId}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const showPlayLoader = computed(
|
||||
() => !isInitialized.value || (shouldLoadHeavyDeps.value && !isModelLoaded.value)
|
||||
)
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const type = displayType.value
|
||||
|
||||
try {
|
||||
if (!streamId.value) {
|
||||
throw new Error('Stream ID must be specified')
|
||||
}
|
||||
|
||||
if (type === 'stream') {
|
||||
// Resolve the stream's first commit
|
||||
const res = await apollo.client.query({
|
||||
query: StreamFirstCommitDocument,
|
||||
variables: {
|
||||
id: streamId.value
|
||||
}
|
||||
})
|
||||
|
||||
if (!res.data.stream?.commits?.items)
|
||||
throw new Error('Could not resolve stream')
|
||||
|
||||
if (
|
||||
res.data.stream.commits.totalCount === 0 ||
|
||||
!res.data.stream.commits.items[0]
|
||||
)
|
||||
throw new Error('Stream has no commits.')
|
||||
|
||||
resourceMetadata.value = {
|
||||
resourceId: res.data.stream.commits.items[0].id,
|
||||
objectId: res.data.stream.commits.items[0].referencedObject,
|
||||
type: 'commit'
|
||||
}
|
||||
} else if (type === 'branch' && branchName.value) {
|
||||
// Resolve a stream branch's first commit
|
||||
const res = await apollo.client.query({
|
||||
query: StreamBranchFirstCommitDocument,
|
||||
variables: {
|
||||
id: streamId.value,
|
||||
branch: branchName.value
|
||||
}
|
||||
})
|
||||
|
||||
if (!res.data.stream?.branch?.commits?.items)
|
||||
throw new Error('Could not resolve stream or its branch')
|
||||
|
||||
if (
|
||||
res.data.stream.branch.commits.totalCount === 0 ||
|
||||
!res.data.stream.branch.commits.items[0]
|
||||
)
|
||||
throw new Error('Branch has no commits.')
|
||||
|
||||
resourceMetadata.value = {
|
||||
resourceId: res.data.stream.branch.commits.items[0].id,
|
||||
objectId: res.data.stream.branch.commits.items[0].referencedObject,
|
||||
type: 'commit'
|
||||
}
|
||||
} else if (type === 'commit' && commitId.value) {
|
||||
resourceMetadata.value = {
|
||||
resourceId: commitId.value,
|
||||
type: 'commit'
|
||||
}
|
||||
} else if (type === 'object' && objectId.value) {
|
||||
resourceMetadata.value = {
|
||||
resourceId: objectId.value,
|
||||
objectId: objectId.value,
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
|
||||
if (!resourceMetadata.value) {
|
||||
throw new Error('Unexpected display type or invalid parameters')
|
||||
}
|
||||
|
||||
// Mark as ready for loading
|
||||
isInitialized.value = true
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err : new Error('Unexpected error')
|
||||
isInitialized.value = shouldLoadHeavyDeps.value = isModelLoaded.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const updateTransparency = () => {
|
||||
const classList = document.getElementById('app')!.classList
|
||||
|
||||
if (transparent.value) {
|
||||
classList.remove('theme--dark')
|
||||
classList.remove('theme--light')
|
||||
} else {
|
||||
const isDarkMode = isDarkTheme()
|
||||
classList.add(`theme--${isDarkMode ? 'dark' : 'light'}`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => transparent, updateTransparency)
|
||||
onMounted(() => updateTransparency())
|
||||
|
||||
return {
|
||||
displayType,
|
||||
streamId,
|
||||
branchName,
|
||||
commitId,
|
||||
objectId,
|
||||
error,
|
||||
resourceMetadata,
|
||||
isInitialized,
|
||||
isModelLoaded,
|
||||
shouldLoadHeavyDeps,
|
||||
height,
|
||||
transparent,
|
||||
previewUrl,
|
||||
showPlayLoader,
|
||||
onError: (e: unknown) => {
|
||||
error.value = e instanceof Error ? e : new Error('Unexpected error')
|
||||
},
|
||||
onModelsLoaded: () => {
|
||||
isModelLoaded.value = true
|
||||
},
|
||||
load: () => {
|
||||
if (!isInitialized.value || shouldLoadHeavyDeps.value || isModelLoaded.value)
|
||||
return
|
||||
|
||||
shouldLoadHeavyDeps.value = true
|
||||
mixpanel.track('Embedded Model Load', {
|
||||
type: 'action'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
position: fixed;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-img {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/*background-attachment: fixed;*/
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
#viewer-play-btn {
|
||||
@keyframes spinner-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// kinda hacky, but vuetify renders the disabled state in a stupid way that relies
|
||||
// on the background color of the thing behind the button
|
||||
&.v-btn--fab.v-btn--disabled {
|
||||
background-color: #242424 !important;
|
||||
}
|
||||
|
||||
.spinning-icon {
|
||||
animation: spinner-spin 0.5s linear infinite;
|
||||
}
|
||||
}
|
||||
.no-mouse {
|
||||
pointer-events: none;
|
||||
}
|
||||
.mouse {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -82,7 +82,7 @@
|
||||
:md="`${comments.items.length !== 1 ? '6' : '12'}`"
|
||||
:xl="`${comments.items.length !== 1 ? '6' : '12'}`"
|
||||
>
|
||||
<comment-list-item :comment="c" :stream="stream" />
|
||||
<comment-list-item :comment="c" :stream="stream" :stream-id="streamId" />
|
||||
</v-col>
|
||||
<v-col v-if="comments.totalCount === 0" class="mt-5">
|
||||
<div class="d-flex align-center">
|
||||
@@ -132,6 +132,7 @@
|
||||
<script>
|
||||
import { gql } from '@apollo/client/core'
|
||||
import { COMMENT_FULL_INFO_FRAGMENT } from '@/graphql/comments'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
name: 'TheStreamHome',
|
||||
@@ -246,7 +247,7 @@ export default {
|
||||
return 12
|
||||
},
|
||||
loggedIn() {
|
||||
return localStorage.getItem('uuid') !== null
|
||||
return AppLocalStorage.get('uuid') !== null
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
STANDARD_PORTAL_KEYS,
|
||||
buildPortalStateMixin
|
||||
} from '@/main/utils/portalStateManager'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
export default {
|
||||
name: 'TheProfileUser',
|
||||
@@ -95,7 +96,7 @@ export default {
|
||||
},
|
||||
created() {
|
||||
// Move to self profile
|
||||
if (this.$route.params.userId === localStorage.getItem('uuid')) {
|
||||
if (this.$route.params.userId === AppLocalStorage.get('uuid')) {
|
||||
this.$router.replace({ path: '/profile' })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -148,7 +148,11 @@ const routes = [
|
||||
resizableNavbar: true,
|
||||
hideEmailBanner: true
|
||||
},
|
||||
component: () => import('@/main/pages/stream/CommitObjectViewer.vue')
|
||||
component: () => import('@/main/pages/stream/CommitObjectViewer.vue'),
|
||||
props: (route) => ({
|
||||
streamId: route.params.streamId,
|
||||
resourceId: route.params.resourceId
|
||||
})
|
||||
},
|
||||
{
|
||||
path: 'objects/:resourceId*',
|
||||
@@ -158,7 +162,11 @@ const routes = [
|
||||
resizableNavbar: true,
|
||||
hideEmailBanner: true
|
||||
},
|
||||
component: () => import('@/main/pages/stream/CommitObjectViewer.vue')
|
||||
component: () => import('@/main/pages/stream/CommitObjectViewer.vue'),
|
||||
props: (route) => ({
|
||||
streamId: route.params.streamId,
|
||||
resourceId: route.params.resourceId
|
||||
})
|
||||
},
|
||||
{
|
||||
path: 'collaborators/',
|
||||
@@ -293,6 +301,23 @@ const routes = [
|
||||
title: 'Not Found | Speckle'
|
||||
},
|
||||
component: () => import('@/main/pages/common/NotFound.vue')
|
||||
},
|
||||
{
|
||||
path: '/embed',
|
||||
meta: {
|
||||
title: 'Embed View | Speckle'
|
||||
},
|
||||
component: () => import('@/main/layouts/TheBasic.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'viewer-embed',
|
||||
meta: {
|
||||
title: 'Embed View | Speckle'
|
||||
},
|
||||
component: () => import('@/main/pages/stream/TheEmbed.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -320,7 +345,8 @@ function shouldForceToLogin(isLoggedIn, to) {
|
||||
'Register',
|
||||
'Error',
|
||||
'Reset Password',
|
||||
'Reset Password Finalization'
|
||||
'Reset Password Finalization',
|
||||
'viewer-embed'
|
||||
]
|
||||
|
||||
// Check if any of the new routes (nested or not) is one of the routes that is allowed for unauthed users
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import emojis from './emojis'
|
||||
Vue.use(Vuex)
|
||||
|
||||
// Note: this is currently used only for 3d viewer filtering purposes. All other state
|
||||
// is handled by apollo. Ideally we would only add extra app state in here if really
|
||||
// necessary (ie, component local state + events is not enough).
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
viewerBusy: false,
|
||||
appliedFilter: null,
|
||||
isolateKey: null,
|
||||
isolateValues: [],
|
||||
hideKey: null,
|
||||
hideValues: [],
|
||||
colorLegend: {},
|
||||
isolateCategoryKey: null,
|
||||
isolateCategoryValues: [],
|
||||
hideCategoryKey: null,
|
||||
hideCategoryValues: [],
|
||||
selectedComment: null,
|
||||
addingComment: false,
|
||||
preventCommentCollapse: false,
|
||||
commentReactions: ['❤️', '✏️', '🔥', '⚠️'],
|
||||
emojis
|
||||
},
|
||||
mutations: {
|
||||
setViewerBusy(state, { viewerBusyState }) {
|
||||
state.viewerBusy = viewerBusyState
|
||||
},
|
||||
setAddingCommentState(state, { addingCommentState }) {
|
||||
state.addingComment = addingCommentState
|
||||
},
|
||||
setCommentSelection(state, { comment }) {
|
||||
if (comment) window.__viewer.interactions.deselectObjects()
|
||||
state.selectedComment = comment
|
||||
},
|
||||
isolateObjects(state, { filterKey, filterValues }) {
|
||||
state.hideKey = null
|
||||
state.hideValues = []
|
||||
if (state.isolateKey !== filterKey) state.isolateValues = []
|
||||
|
||||
state.isolateKey = filterKey
|
||||
state.isolateValues = [...new Set([...state.isolateValues, ...filterValues])]
|
||||
if (state.isolateValues.length === 0) state.appliedFilter = null
|
||||
else
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { includes: state.isolateValues } },
|
||||
ghostOthers: true
|
||||
}
|
||||
window.__viewer.applyFilter(state.appliedFilter)
|
||||
},
|
||||
unisolateObjects(state, { filterKey, filterValues }) {
|
||||
state.hideKey = null
|
||||
state.hideValues = []
|
||||
if (state.isolateKey !== filterKey) state.isolateValues = []
|
||||
|
||||
state.isolateKey = filterKey
|
||||
state.isolateValues = state.isolateValues.filter(
|
||||
(val) => filterValues.indexOf(val) === -1
|
||||
)
|
||||
if (state.isolateValues.length === 0) state.appliedFilter = null
|
||||
else
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { includes: state.isolateValues } },
|
||||
ghostOthers: true
|
||||
}
|
||||
window.__viewer.applyFilter(state.appliedFilter)
|
||||
},
|
||||
hideObjects(state, { filterKey, filterValues }) {
|
||||
state.isolateKey = null
|
||||
state.isolateValues = []
|
||||
if (state.hideKey !== filterKey) state.hideValues = []
|
||||
|
||||
state.hideKey = filterKey
|
||||
state.hideValues = [...new Set([...filterValues, ...state.hideValues])]
|
||||
|
||||
if (state.hideValues.length === 0) state.appliedFilter = null
|
||||
else
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { excludes: state.hideValues } }
|
||||
}
|
||||
window.__viewer.applyFilter(state.appliedFilter)
|
||||
},
|
||||
showObjects(state, { filterKey, filterValues }) {
|
||||
state.isolateKey = null
|
||||
state.isolateValues = []
|
||||
if (state.hideKey !== filterKey) state.hideValues = []
|
||||
|
||||
state.hideKey = filterKey
|
||||
state.hideValues = state.hideValues.filter(
|
||||
(val) => filterValues.indexOf(val) === -1
|
||||
)
|
||||
|
||||
if (state.hideValues.length === 0) state.appliedFilter = null
|
||||
else
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { excludes: state.hideValues } }
|
||||
}
|
||||
window.__viewer.applyFilter(state.appliedFilter)
|
||||
},
|
||||
async isolateCategoryToggle(
|
||||
state,
|
||||
{ filterKey, filterValue, allValues, colorBy = false }
|
||||
) {
|
||||
this.commit('resetInternalHideIsolateObjectState')
|
||||
state.hideCategoryKey = null
|
||||
state.hideCategoryValues = []
|
||||
|
||||
if (filterKey !== state.isolateCategoryKey) state.isolateCategoryValues = []
|
||||
state.isolateCategoryKey = filterKey
|
||||
|
||||
const indx = state.isolateCategoryValues.indexOf(filterValue)
|
||||
if (indx === -1) state.isolateCategoryValues.push(filterValue)
|
||||
else state.isolateCategoryValues.splice(indx, 1)
|
||||
|
||||
if (
|
||||
(state.isolateCategoryValues.length === 0 ||
|
||||
state.isolateCategoryValues.length === allValues.length) &&
|
||||
!colorBy
|
||||
) {
|
||||
state.appliedFilter = null
|
||||
window.__viewer.applyFilter(state.appliedFilter)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.isolateCategoryValues.length === 0 && colorBy) {
|
||||
state.appliedFilter = {
|
||||
colorBy: { type: 'category', property: filterKey }
|
||||
}
|
||||
}
|
||||
if (state.isolateCategoryValues.length !== 0) {
|
||||
state.appliedFilter = {
|
||||
ghostOthers: true,
|
||||
filterBy: { [filterKey]: state.isolateCategoryValues },
|
||||
colorBy: colorBy ? { type: 'category', property: filterKey } : null
|
||||
}
|
||||
}
|
||||
if (state.isolateCategoryValues.length === allValues.length)
|
||||
delete state.appliedFilter.filterBy
|
||||
const res = await window.__viewer.applyFilter(state.appliedFilter)
|
||||
state.colorLegend = res.colorLegend
|
||||
},
|
||||
async hideCategoryToggle(state, { filterKey, filterValue, colorBy = false }) {
|
||||
this.commit('resetInternalHideIsolateObjectState')
|
||||
state.isolateCategoryKey = null
|
||||
state.isolateCategoryValues = []
|
||||
if (filterKey !== state.hideCategoryKey) state.hideCategoryValues = []
|
||||
state.hideCategoryKey = filterKey
|
||||
|
||||
const indx = state.hideCategoryValues.indexOf(filterValue)
|
||||
if (indx === -1) state.hideCategoryValues.push(filterValue)
|
||||
else state.hideCategoryValues.splice(indx, 1)
|
||||
|
||||
if (state.hideCategoryValues.length === 0 && !colorBy) {
|
||||
state.appliedFilter = null
|
||||
window.__viewer.applyFilter(state.appliedFilter)
|
||||
return
|
||||
}
|
||||
|
||||
if (state.hideCategoryValues.length === 0 && colorBy) {
|
||||
state.appliedFilter = {
|
||||
colorBy: { type: 'category', property: filterKey }
|
||||
}
|
||||
}
|
||||
if (state.hideCategoryValues.length !== 0) {
|
||||
state.appliedFilter = {
|
||||
filterBy: { [filterKey]: { not: state.hideCategoryValues } },
|
||||
colorBy: colorBy ? { type: 'category', property: filterKey } : null
|
||||
}
|
||||
}
|
||||
const res = await window.__viewer.applyFilter(state.appliedFilter)
|
||||
state.colorLegend = res.colorLegend
|
||||
},
|
||||
async toggleColorByCategory(state, { filterKey }) {
|
||||
if (state.appliedFilter && state.appliedFilter.colorBy) {
|
||||
state.appliedFilter.colorBy = null
|
||||
} else
|
||||
state.appliedFilter = {
|
||||
...state.appliedFilter,
|
||||
colorBy: { type: 'category', property: filterKey }
|
||||
}
|
||||
const res = await window.__viewer.applyFilter(state.appliedFilter)
|
||||
state.colorLegend = res.colorLegend
|
||||
},
|
||||
setNumericFilter(
|
||||
state,
|
||||
{ filterKey, minValue, maxValue, gradientColors = ['#3F5EFB', '#FC466B'] }
|
||||
) {
|
||||
this.commit('resetInternalHideIsolateObjectState')
|
||||
this.commit('resetInternalCategoryObjectState')
|
||||
state.appliedFilter = {
|
||||
ghostOthers: true,
|
||||
colorBy: {
|
||||
type: 'gradient',
|
||||
property: filterKey,
|
||||
minValue,
|
||||
maxValue,
|
||||
gradientColors
|
||||
},
|
||||
filterBy: { [filterKey]: { gte: minValue, lte: maxValue } }
|
||||
}
|
||||
window.__viewer.applyFilter(state.appliedFilter)
|
||||
},
|
||||
async setFilterDirect(state, { filter }) {
|
||||
const filterBy = filter.filterBy
|
||||
if (filterBy && filterBy.__parents) {
|
||||
if (filterBy.__parents.includes) {
|
||||
this.commit('isolateObjects', {
|
||||
filterKey: '__parents',
|
||||
filterValues: filterBy.__parents.includes
|
||||
})
|
||||
return
|
||||
}
|
||||
if (filterBy.__parents.excludes) {
|
||||
this.commit('hideObjects', {
|
||||
filterKey: '__parents',
|
||||
filterValues: filterBy.__parents.excludes
|
||||
})
|
||||
return
|
||||
}
|
||||
} else if (filter.ghostOthers) {
|
||||
// means it's isolate by category or numeric filter
|
||||
if (filter.colorBy && filter.colorBy.type === 'gradient') {
|
||||
this.commit('setNumericFilter', {
|
||||
filterKey: Object.keys(filter.filterBy)[0],
|
||||
minValue: filter.filterBy[Object.keys(filter.filterBy)[0]].gte,
|
||||
maxValue: filter.filterBy[Object.keys(filter.filterBy)[0]].lte
|
||||
})
|
||||
} else {
|
||||
const values = filterBy[Object.keys(filter.filterBy)[0]]
|
||||
for (const val of values) {
|
||||
const f = {
|
||||
filterKey: Object.keys(filter.filterBy)[0],
|
||||
filterValue: val,
|
||||
allValues: [],
|
||||
colorBy: filter.colorBy
|
||||
}
|
||||
this.commit('isolateCategoryToggle', f)
|
||||
}
|
||||
}
|
||||
} else if (filterBy) {
|
||||
const values = filterBy[Object.keys(filter.filterBy)[0]].not
|
||||
for (const val of values) {
|
||||
const f = {
|
||||
filterKey: Object.keys(filter.filterBy)[0],
|
||||
filterValue: val,
|
||||
allValues: [],
|
||||
colorBy: filter.colorBy
|
||||
}
|
||||
this.commit('hideCategoryToggle', f)
|
||||
}
|
||||
} else if (filter.colorBy) {
|
||||
this.commit('toggleColorByCategory', { filterKey: filter.colorBy.property })
|
||||
}
|
||||
},
|
||||
resetInternalHideIsolateObjectState(state) {
|
||||
state.isolateKey = null
|
||||
state.isolateValues = []
|
||||
state.hideKey = null
|
||||
state.hideValues = []
|
||||
},
|
||||
resetInternalCategoryObjectState(state) {
|
||||
state.isolateCategoryKey = null
|
||||
state.isolateCategoryValues = []
|
||||
state.hideCategoryKey = null
|
||||
state.hideCategoryValues = []
|
||||
},
|
||||
resetFilter(state) {
|
||||
this.commit('resetInternalHideIsolateObjectState')
|
||||
this.commit('resetInternalCategoryObjectState')
|
||||
state.appliedFilter = null
|
||||
state.preventCommentCollapse = true
|
||||
window.__viewer.applyFilter(state.appliedFilter)
|
||||
},
|
||||
setPreventCommentCollapse(state, { value }) {
|
||||
state.preventCommentCollapse = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
import Vue from 'vue'
|
||||
|
||||
/**
|
||||
@@ -16,11 +17,7 @@ export function setDarkTheme(val, save = false) {
|
||||
themeState.dark = !!val
|
||||
|
||||
if (!save) return
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, val ? THEME_DARK : THEME_LIGHT)
|
||||
} catch (e) {
|
||||
// Suppressing missing localStorage errors
|
||||
}
|
||||
AppLocalStorage.set(LOCAL_STORAGE_KEY, val ? THEME_DARK : THEME_LIGHT)
|
||||
}
|
||||
|
||||
export function isDarkTheme() {
|
||||
@@ -28,14 +25,10 @@ export function isDarkTheme() {
|
||||
}
|
||||
|
||||
export function initialize() {
|
||||
try {
|
||||
const storageSetting = localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
if (storageSetting) {
|
||||
setDarkTheme(storageSetting === THEME_DARK)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
// Suppressing missing localStorage errors
|
||||
const storageSetting = AppLocalStorage.get(LOCAL_STORAGE_KEY)
|
||||
if (storageSetting) {
|
||||
setDarkTheme(storageSetting === THEME_DARK)
|
||||
return
|
||||
}
|
||||
|
||||
const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
@@ -2,18 +2,13 @@ import { mainUserDataQuery } from '@/graphql/user'
|
||||
import { LocalStorageKeys } from '@/helpers/mainConstants'
|
||||
import md5 from '@/helpers/md5'
|
||||
import { VALID_EMAIL_REGEX } from '@/main/lib/common/vuetify/validators'
|
||||
import { AppLocalStorage } from '@/utils/localStorage'
|
||||
|
||||
const appId = 'spklwebapp'
|
||||
const appSecret = 'spklwebapp'
|
||||
|
||||
export function getAuthToken() {
|
||||
try {
|
||||
return localStorage.getItem(LocalStorageKeys.AuthToken)
|
||||
} catch (e) {
|
||||
// suppressed localStorage errors
|
||||
}
|
||||
|
||||
return null
|
||||
return AppLocalStorage.get(LocalStorageKeys.AuthToken)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,8 +21,8 @@ export async function checkAccessCodeAndGetTokens() {
|
||||
const response = await getTokenFromAccessCode(accessCode)
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (response.hasOwnProperty('token')) {
|
||||
localStorage.setItem(LocalStorageKeys.AuthToken, response.token)
|
||||
localStorage.setItem(LocalStorageKeys.RefreshToken, response.refreshToken)
|
||||
AppLocalStorage.set(LocalStorageKeys.AuthToken, response.token)
|
||||
AppLocalStorage.set(LocalStorageKeys.RefreshToken, response.refreshToken)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
@@ -41,7 +36,7 @@ export async function checkAccessCodeAndGetTokens() {
|
||||
* @return {Object} The full graphql response.
|
||||
*/
|
||||
export async function prefetchUserAndSetSuuid(apolloClient) {
|
||||
const token = localStorage.getItem(LocalStorageKeys.AuthToken)
|
||||
const token = AppLocalStorage.get(LocalStorageKeys.AuthToken)
|
||||
if (!token) return
|
||||
|
||||
// Pull user info (& remember it in the Apollo cache)
|
||||
@@ -53,10 +48,10 @@ export async function prefetchUserAndSetSuuid(apolloClient) {
|
||||
// eslint-disable-next-line camelcase
|
||||
const distinct_id = '@' + md5(data.user.email.toLowerCase()).toUpperCase()
|
||||
|
||||
localStorage.setItem('suuid', data.user.suuid)
|
||||
localStorage.setItem('distinct_id', distinct_id)
|
||||
localStorage.setItem('uuid', data.user.id)
|
||||
localStorage.setItem('stcount', data.user.streams.totalCount)
|
||||
AppLocalStorage.set('suuid', data.user.suuid)
|
||||
AppLocalStorage.set('distinct_id', distinct_id)
|
||||
AppLocalStorage.set('uuid', data.user.id)
|
||||
AppLocalStorage.set('stcount', data.user.streams.totalCount)
|
||||
return data
|
||||
} else {
|
||||
await signOut()
|
||||
@@ -74,7 +69,7 @@ export async function getTokenFromAccessCode(accessCode) {
|
||||
accessCode,
|
||||
appId,
|
||||
appSecret,
|
||||
challenge: localStorage.getItem('appChallenge')
|
||||
challenge: AppLocalStorage.get('appChallenge')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -93,18 +88,18 @@ export async function signOut(mixpanelInstance) {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: localStorage.getItem(LocalStorageKeys.AuthToken),
|
||||
refreshToken: localStorage.getItem(LocalStorageKeys.RefreshToken)
|
||||
token: AppLocalStorage.get(LocalStorageKeys.AuthToken),
|
||||
refreshToken: AppLocalStorage.get(LocalStorageKeys.RefreshToken)
|
||||
})
|
||||
})
|
||||
|
||||
localStorage.removeItem(LocalStorageKeys.AuthToken)
|
||||
localStorage.removeItem(LocalStorageKeys.RefreshToken)
|
||||
localStorage.removeItem('suuid')
|
||||
localStorage.removeItem('uuid')
|
||||
localStorage.removeItem('distinct_id')
|
||||
localStorage.removeItem('stcount')
|
||||
localStorage.removeItem('onboarding')
|
||||
AppLocalStorage.remove(LocalStorageKeys.AuthToken)
|
||||
AppLocalStorage.remove(LocalStorageKeys.RefreshToken)
|
||||
AppLocalStorage.remove('suuid')
|
||||
AppLocalStorage.remove('uuid')
|
||||
AppLocalStorage.remove('distinct_id')
|
||||
AppLocalStorage.remove('stcount')
|
||||
AppLocalStorage.remove('onboarding')
|
||||
|
||||
window.location = '/'
|
||||
|
||||
@@ -115,7 +110,7 @@ export async function signOut(mixpanelInstance) {
|
||||
}
|
||||
|
||||
export async function refreshToken() {
|
||||
const refreshToken = localStorage.getItem(LocalStorageKeys.RefreshToken)
|
||||
const refreshToken = AppLocalStorage.get(LocalStorageKeys.RefreshToken)
|
||||
if (!refreshToken) throw new Error('No refresh token found')
|
||||
|
||||
const refreshResponse = await fetch('/auth/token', {
|
||||
@@ -134,8 +129,8 @@ export async function refreshToken() {
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (data.hasOwnProperty('token')) {
|
||||
localStorage.setItem(LocalStorageKeys.AuthToken, data.token)
|
||||
localStorage.setItem(LocalStorageKeys.RefreshToken, data.refreshToken)
|
||||
AppLocalStorage.set(LocalStorageKeys.AuthToken, data.token)
|
||||
AppLocalStorage.set(LocalStorageKeys.RefreshToken, data.refreshToken)
|
||||
await prefetchUserAndSetSuuid()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function resourceType(resourceId) {
|
||||
return resourceId.length === 10 ? 'commit' : 'object'
|
||||
}
|
||||
@@ -250,3 +250,20 @@ html {
|
||||
scrollbar-color: $sb-color;
|
||||
}
|
||||
}
|
||||
|
||||
// v-navigation-drawer resizer global restyle
|
||||
.nav-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
z-index: 100000;
|
||||
transition: all 0.6s ease;
|
||||
opacity: 0.01;
|
||||
border: 4px solid royalblue;
|
||||
}
|
||||
.nav-resizer:hover {
|
||||
opacity: 0.5;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
declare module '*.gql' {
|
||||
import { DocumentNode } from '@apollo/client/core'
|
||||
|
||||
const operation: DocumentNode
|
||||
export default operation
|
||||
}
|
||||
+1
-1
@@ -23,7 +23,7 @@ declare module 'vue/types/vue' {
|
||||
|
||||
/**
|
||||
* Check if auth token is stored in localStorage
|
||||
* @deprecated Use `isLoggedInQuery`/`isLoggedInMixin` instead
|
||||
* @deprecated Use `isLoggedInQuery`/`isLoggedInMixin`/`useIsLoggedIn` instead
|
||||
*/
|
||||
$loggedIn: () => boolean
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ export {}
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* Initialized in SpeckleViewer.vue
|
||||
* Used in some layout components
|
||||
*/
|
||||
__viewer?: import('@speckle/viewer').Viewer
|
||||
__lastNavSize?: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ const config = {
|
||||
|
||||
// Setting source map according to build env
|
||||
config.devtool(isProdBuild ? false : 'eval-source-map')
|
||||
|
||||
// Enable .mjs support
|
||||
config.module
|
||||
.rule('mjs-support')
|
||||
.test(/\.mjs$/)
|
||||
.type('javascript/auto')
|
||||
.include.add(/node_modules/)
|
||||
.end()
|
||||
},
|
||||
productionSourceMap: false,
|
||||
pages: {
|
||||
@@ -36,12 +44,6 @@ const config = {
|
||||
title: 'Speckle',
|
||||
template: 'public/app.html',
|
||||
filename: 'app.html'
|
||||
},
|
||||
embedApp: {
|
||||
entry: 'src/embed/embedApp.js',
|
||||
title: 'Speckle Embed Viewer',
|
||||
template: 'public/embedApp.html',
|
||||
filename: 'embedApp.html'
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
@@ -50,7 +52,6 @@ const config = {
|
||||
historyApiFallback: {
|
||||
rewrites: [
|
||||
{ from: /^\/$/, to: '/app.html' },
|
||||
{ from: /\/embed(.*)/, to: '/embedApp.html' },
|
||||
{ from: /./, to: '/app.html' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -303,12 +303,11 @@ export class Viewer extends EventEmitter implements IViewer {
|
||||
}
|
||||
|
||||
public async unloadAll() {
|
||||
for (const key of Object.keys(this.loaders)) {
|
||||
await this.loaders[key].unload()
|
||||
delete this.loaders[key]
|
||||
}
|
||||
const loaders = Object.values(this.loaders)
|
||||
this.loaders = {}
|
||||
|
||||
await Promise.all(loaders.map((l) => l.unload()))
|
||||
await this.applyFilter(null)
|
||||
return
|
||||
}
|
||||
|
||||
public async applyFilter(filter: unknown) {
|
||||
|
||||
@@ -4855,11 +4855,11 @@ __metadata:
|
||||
"@vue/cli-plugin-babel": ~4.3.1
|
||||
"@vue/cli-plugin-router": ~4.3.1
|
||||
"@vue/cli-plugin-typescript": ~4.5.19
|
||||
"@vue/cli-plugin-vuex": ~4.3.1
|
||||
"@vue/cli-service": ~4.3.1
|
||||
"@vue/eslint-config-typescript": ^11.0.0
|
||||
"@vuejs-community/vue-filter-date-format": ^1.6.3
|
||||
"@vuejs-community/vue-filter-date-parse": ^1.1.6
|
||||
"@vueuse/core": ^9.0.0
|
||||
apexcharts: ^3.33.1
|
||||
apollo-upload-client: ^17.0.0
|
||||
babel-eslint: ^10.1.0
|
||||
@@ -4903,7 +4903,6 @@ __metadata:
|
||||
vuetify: ^2.3.21
|
||||
vuetify-image-input: ^19.1.0
|
||||
vuetify-loader: ^1.9.1
|
||||
vuex: ^3.6.2
|
||||
webpack: ^4.46.0
|
||||
webpack-bundle-analyzer: ^4.5.0
|
||||
languageName: unknown
|
||||
@@ -6011,6 +6010,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/web-bluetooth@npm:^0.0.15":
|
||||
version: 0.0.15
|
||||
resolution: "@types/web-bluetooth@npm:0.0.15"
|
||||
checksum: 4e3b3b1c0baf6735690ce0c5ffaac53de3dbd16362316cbc5e32970bcb1e1baf16dae0a9f30fe86256ad0ee22a4533423f443835273efc54b15235086ebda85b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/webpack-env@npm:^1.15.2":
|
||||
version: 1.17.0
|
||||
resolution: "@types/webpack-env@npm:1.17.0"
|
||||
@@ -6592,15 +6598,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/cli-plugin-vuex@npm:~4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "@vue/cli-plugin-vuex@npm:4.3.1"
|
||||
peerDependencies:
|
||||
"@vue/cli-service": ^3.0.0 || ^4.0.0-0
|
||||
checksum: 0de3d1b4798793273527ffe6435a070c13376b058b009c6dbc53fa23eda64ac1a8ffed9f097a5334e863cb7dbd782d4112b918c30df34988e0c5ce916e3fa571
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/cli-service@npm:~4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "@vue/cli-service@npm:4.3.1"
|
||||
@@ -6926,6 +6923,34 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vueuse/core@npm:^9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "@vueuse/core@npm:9.0.0"
|
||||
dependencies:
|
||||
"@types/web-bluetooth": ^0.0.15
|
||||
"@vueuse/metadata": 9.0.0
|
||||
"@vueuse/shared": 9.0.0
|
||||
vue-demi: "*"
|
||||
checksum: 2825bad3f90bf030a23e4f14d67018a9b3497e672fa61fc555da483488bb4df28d61e2141637d6ed97a3d0c8544f60e7474ebf94d7deb6933199727505cba08b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vueuse/metadata@npm:9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "@vueuse/metadata@npm:9.0.0"
|
||||
checksum: 13404823d085df4d5221a7be4c8847915d7682888d9465aad3b257e511a509e1baf99575236cdfd2d3cc1abaaddf4180ab5e6e63fb54d6dfbf007c099e0020b3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vueuse/shared@npm:9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "@vueuse/shared@npm:9.0.0"
|
||||
dependencies:
|
||||
vue-demi: "*"
|
||||
checksum: 4925d94e53d27174757e153ac3fc1f350633399faa2a8f4e6cf323b5ccb0d106e2415e050664c3fe325932464bfaaad7ea9d38afb2ffa219e35f6accd64b2ee6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@webassemblyjs/ast@npm:1.11.1":
|
||||
version: 1.11.1
|
||||
resolution: "@webassemblyjs/ast@npm:1.11.1"
|
||||
@@ -26774,6 +26799,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vue-demi@npm:*":
|
||||
version: 0.13.6
|
||||
resolution: "vue-demi@npm:0.13.6"
|
||||
peerDependencies:
|
||||
"@vue/composition-api": ^1.0.0-rc.1
|
||||
vue: ^3.0.0-0 || ^2.6.0
|
||||
peerDependenciesMeta:
|
||||
"@vue/composition-api":
|
||||
optional: true
|
||||
bin:
|
||||
vue-demi-fix: bin/vue-demi-fix.js
|
||||
vue-demi-switch: bin/vue-demi-switch.js
|
||||
checksum: 7712b1f8edad902a8ea99ac307f9dc276ca13cb4394fdc6753c27c32f16e5f4a6d3acaa752bf24d73be0ea972552ad7da4c61314b046fbc248bd9aa26afe2e59
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vue-demi@npm:^0.13.1":
|
||||
version: 0.13.5
|
||||
resolution: "vue-demi@npm:0.13.5"
|
||||
@@ -27028,15 +27069,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vuex@npm:^3.6.2":
|
||||
version: 3.6.2
|
||||
resolution: "vuex@npm:3.6.2"
|
||||
peerDependencies:
|
||||
vue: ^2.0.0
|
||||
checksum: 37915741ba9557024ea42579e2c8d81847ec699d07d8ea3aa673929f26fceba8c59e7e1e6a875fd3962e50018bc30f782f5ace34d83307ef228ad46a4df343da
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"w3c-keyname@npm:^2.2.0":
|
||||
version: 2.2.4
|
||||
resolution: "w3c-keyname@npm:2.2.4"
|
||||
|
||||
Reference in New Issue
Block a user